| /** | |
| * Formats CLI help and errors as text. | |
| * | |
| * This module turns help documents, CLI errors, grouped errors, and version | |
| * information into strings. It does not write those strings to the terminal | |
| * itself. It includes the `Formatter` interface, the formatter service, a layer | |
| * for custom formatters, and the default formatter with configurable color | |
| * support. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| import * as Context from "../../Context.js"; | |
| import * as Layer from "../../Layer.js"; | |
| import * as Option from "../../Option.js"; | |
| /** | |
| * Service reference for the CLI output formatter. Provides a default implementation | |
| * that can be overridden for custom formatting or testing. | |
| * | |
| * **Example** (Accessing the output formatter) | |
| * | |
| * ```ts | |
| * import { Effect } from "effect" | |
| * import { CliOutput } from "effect/unstable/cli" | |
| * | |
| * // Access the formatter service | |
| * const program = Effect.gen(function*() { | |
| * const formatter = yield* CliOutput.Formatter | |
| * | |
| * // Format version information | |
| * const versionText = formatter.formatVersion("my-cli", "2.1.0") | |
| * console.log(versionText) // "my-cli v2.1.0" (with colors if supported) | |
| * | |
| * return versionText | |
| * }) | |
| * | |
| * // Run with default formatter | |
| * const result = Effect.runSync(program) | |
| * ``` | |
| * | |
| * @category services | |
| * @since 4.0.0 | |
| */ | |
| export const Formatter = /*#__PURE__*/Context.Reference("effect/cli/CliOutput", { | |
| defaultValue: () => defaultFormatter() | |
| }); | |
| /** | |
| * Creates a Layer that provides a custom Formatter implementation. | |
| * | |
| * **Example** (Providing a custom formatter) | |
| * | |
| * ```ts | |
| * import { Console, Effect } from "effect" | |
| * import { CliOutput } from "effect/unstable/cli" | |
| * | |
| * // Create a custom formatter without colors | |
| * const noColorFormatter = CliOutput.defaultFormatter({ colors: false }) | |
| * const NoColorLayer = CliOutput.layer(noColorFormatter) | |
| * | |
| * // Create a program that uses the custom formatter | |
| * const program = Effect.gen(function*() { | |
| * const formatter = yield* CliOutput.Formatter | |
| * const versionText = formatter.formatVersion("my-cli", "1.0.0") | |
| * yield* Console.log(`Using custom formatter: ${versionText}`) | |
| * }).pipe( | |
| * Effect.provide(NoColorLayer) | |
| * ) | |
| * | |
| * // You can also create completely custom formatters | |
| * const jsonFormatter: CliOutput.Formatter = { | |
| * formatHelpDoc: (doc) => JSON.stringify(doc, null, 2), | |
| * formatCliError: (error) => JSON.stringify({ error: error.message }), | |
| * formatError: (error) => | |
| * JSON.stringify({ type: "error", message: error.message }), | |
| * formatVersion: (name, version) => JSON.stringify({ name, version }), | |
| * formatErrors: (errors) => JSON.stringify(errors.map((error) => error.message)) | |
| * } | |
| * const JsonLayer = CliOutput.layer(jsonFormatter) | |
| * ``` | |
| * | |
| * @category layers | |
| * @since 4.0.0 | |
| */ | |
| export const layer = formatter => Layer.succeed(Formatter)(formatter); | |
| /** | |
| * Creates a default formatter with configurable options. | |
| * | |
| * **Example** (Creating default formatters) | |
| * | |
| * ```ts | |
| * import { Effect } from "effect" | |
| * import { CliError, CliOutput } from "effect/unstable/cli" | |
| * | |
| * // Create a formatter without colors for tests or CI environments | |
| * const noColorFormatter = CliOutput.defaultFormatter({ colors: false }) | |
| * | |
| * // Create a formatter with colors forced on | |
| * const colorFormatter = CliOutput.defaultFormatter({ colors: true }) | |
| * | |
| * // Auto-detect colors based on terminal support (default behavior) | |
| * const autoFormatter = CliOutput.defaultFormatter() | |
| * | |
| * const program = Effect.gen(function*() { | |
| * const formatter = colorFormatter | |
| * | |
| * // Format an error with proper styling | |
| * const error = new CliError.InvalidValue({ | |
| * option: "foo", | |
| * value: "bar", | |
| * expected: "baz", | |
| * kind: "flag" | |
| * }) | |
| * const errorText = formatter.formatError(error) | |
| * console.log(errorText) | |
| * | |
| * // Format version information | |
| * const versionText = formatter.formatVersion("my-tool", "1.2.3") | |
| * console.log(versionText) | |
| * }) | |
| * ``` | |
| * | |
| * @category constructors | |
| * @since 4.0.0 | |
| */ | |
| export const defaultFormatter = options => { | |
| const globalProcess = globalThis.process; | |
| const hasProcess = typeof globalProcess === "object" && globalProcess !== null; | |
| const useColor = options?.colors !== undefined ? options.colors | |
| // Auto-detect based on environment | |
| : hasProcess && typeof globalProcess.stdout === "object" && globalProcess.stdout !== null && globalProcess.stdout.isTTY === true && globalProcess.env?.NO_COLOR !== "1"; | |
| // Color palette using ANSI escape codes | |
| const colors = useColor ? { | |
| bold: text => `\x1b[1m${text}\x1b[0m`, | |
| dim: text => `\x1b[2m${text}\x1b[0m`, | |
| cyan: text => `\x1b[36m${text}\x1b[0m`, | |
| green: text => `\x1b[32m${text}\x1b[0m`, | |
| blue: text => `\x1b[34m${text}\x1b[0m`, | |
| yellow: text => `\x1b[33m${text}\x1b[0m`, | |
| magenta: text => `\x1b[35m${text}\x1b[0m` | |
| } : { | |
| bold: text => text, | |
| dim: text => text, | |
| cyan: text => text, | |
| green: text => text, | |
| blue: text => text, | |
| yellow: text => text, | |
| magenta: text => text | |
| }; | |
| const reset = useColor ? "\x1b[0m" : ""; | |
| const red = useColor ? "\x1b[31m" : ""; | |
| const bold = useColor ? "\x1b[1m" : ""; | |
| return { | |
| formatHelpDoc: doc => formatHelpDocImpl(doc, colors), | |
| formatCliError: error => error.message, | |
| formatError: error => { | |
| return `\n${bold}${red}ERROR${reset}\n ${error.message}${reset}`; | |
| }, | |
| formatErrors: errors => { | |
| if (errors.length === 0) return ""; | |
| if (errors.length === 1) { | |
| return `\n${bold}${red}ERROR${reset}\n ${errors[0].message}${reset}`; | |
| } | |
| // Group errors by _tag | |
| const grouped = new Map(); | |
| for (const error of errors) { | |
| const tag = error._tag ?? "Error"; | |
| const group = grouped.get(tag) ?? []; | |
| group.push(error); | |
| grouped.set(tag, group); | |
| } | |
| const sections = []; | |
| sections.push(`\n${bold}${red}ERRORS${reset}`); | |
| for (const [, group] of grouped) { | |
| for (const error of group) { | |
| sections.push(` ${error.message}${reset}`); | |
| } | |
| } | |
| return sections.join("\n"); | |
| }, | |
| formatVersion: (name, version) => `${colors.bold(name)} ${colors.dim("v")}${colors.bold(version)}` | |
| }; | |
| }; | |
| /** | |
| * Strips ANSI escape codes from a string to calculate visual width. | |
| * @internal | |
| */ | |
| const stripAnsi = text => { | |
| // oxlint-disable-next-line no-control-regex | |
| return text.replace(/\u001B\[[0-9;]*m/g, ""); | |
| }; | |
| /** | |
| * Gets the visual length of a string (excluding ANSI codes). | |
| * @internal | |
| */ | |
| const visualLength = text => stripAnsi(text).length; | |
| /** | |
| * Helper function to pad strings to a specified width. | |
| * @internal | |
| */ | |
| const pad = (s, width) => { | |
| const actualLength = visualLength(s); | |
| const padding = Math.max(0, width - actualLength); | |
| return s + " ".repeat(padding); | |
| }; | |
| /** | |
| * Renders a table with aligned columns. | |
| * @internal | |
| */ | |
| const renderTable = (rows, widthCap) => { | |
| const maxColumn = Math.max(...rows.map(r => visualLength(r.left))) + 4; | |
| const col = widthCap === undefined ? maxColumn : Math.min(maxColumn, widthCap); | |
| return rows.map(({ | |
| left, | |
| right | |
| }) => ` ${pad(left, col)}${right}`).join("\n"); | |
| }; | |
| const formatSubcommandName = (name, alias) => alias ? `${name}, ${alias}` : name; | |
| /** | |
| * Internal implementation of help formatting that accepts configurable color functions. | |
| * @internal | |
| */ | |
| const formatHelpDocImpl = (doc, colors) => { | |
| const sections = []; | |
| // Description section | |
| if (doc.description) { | |
| sections.push(colors.bold("DESCRIPTION")); | |
| sections.push(` ${doc.description}`); | |
| sections.push(""); | |
| } | |
| // Usage section | |
| sections.push(colors.bold("USAGE")); | |
| sections.push(` ${colors.cyan(doc.usage)}`); | |
| sections.push(""); | |
| // Arguments section | |
| if (doc.args && doc.args.length > 0) { | |
| sections.push(colors.bold("ARGUMENTS")); | |
| const argRows = doc.args.map(arg => { | |
| let name = arg.name; | |
| if (arg.variadic) { | |
| name += "..."; | |
| } | |
| const coloredName = colors.green(name); | |
| const coloredType = colors.dim(arg.type); | |
| const nameType = `${coloredName} ${coloredType}`; | |
| const optionalSuffix = arg.required ? "" : colors.dim(" (optional)"); | |
| const description = Option.getOrElse(arg.description, () => "") + optionalSuffix; | |
| return { | |
| left: nameType, | |
| right: description | |
| }; | |
| }); | |
| sections.push(renderTable(argRows, 25)); | |
| sections.push(""); | |
| } | |
| // Flags section | |
| if (doc.flags.length > 0) { | |
| sections.push(colors.bold("FLAGS")); | |
| const flagRows = doc.flags.map(flag => { | |
| const names = []; | |
| // Add main name with -- prefix first | |
| names.push(colors.green(`--${flag.name}`)); | |
| // Add aliases after (like -f) to match expected ordering | |
| for (const alias of flag.aliases) { | |
| names.push(colors.green(alias)); | |
| } | |
| const namesPart = names.join(", "); | |
| const typePart = flag.type !== "boolean" ? ` ${colors.dim(flag.type)}` : ""; | |
| return { | |
| left: namesPart + typePart, | |
| right: Option.getOrElse(flag.description, () => "") | |
| }; | |
| }); | |
| sections.push(renderTable(flagRows)); | |
| sections.push(""); | |
| } | |
| // Global Flags section | |
| if (doc.globalFlags && doc.globalFlags.length > 0) { | |
| sections.push(colors.bold("GLOBAL FLAGS")); | |
| const globalFlagRows = doc.globalFlags.map(flag => { | |
| const names = []; | |
| // Add main name with -- prefix first | |
| names.push(colors.green(`--${flag.name}`)); | |
| // Add aliases after (like -f) to match expected ordering | |
| for (const alias of flag.aliases) { | |
| names.push(colors.green(alias)); | |
| } | |
| const namesPart = names.join(", "); | |
| const typePart = flag.type !== "boolean" ? ` ${colors.dim(flag.type)}` : ""; | |
| return { | |
| left: namesPart + typePart, | |
| right: Option.getOrElse(flag.description, () => "") | |
| }; | |
| }); | |
| sections.push(renderTable(globalFlagRows)); | |
| sections.push(""); | |
| } | |
| // Subcommands section | |
| if (doc.subcommands && doc.subcommands.length > 0) { | |
| const ungrouped = doc.subcommands.find(group => group.group === undefined); | |
| if (ungrouped) { | |
| sections.push(colors.bold("SUBCOMMANDS")); | |
| sections.push(renderTable(ungrouped.commands.map(sub => ({ | |
| left: colors.cyan(formatSubcommandName(sub.name, sub.alias)), | |
| right: sub.shortDescription ?? sub.description | |
| })), 20)); | |
| if (doc.subcommands.length > 1) { | |
| sections.push(""); | |
| } | |
| } | |
| for (const group of doc.subcommands) { | |
| if (group.group === undefined) continue; | |
| sections.push(colors.bold(`${group.group}:`)); | |
| sections.push(renderTable(group.commands.map(sub => ({ | |
| left: colors.cyan(formatSubcommandName(sub.name, sub.alias)), | |
| right: sub.shortDescription ?? sub.description | |
| })), 20)); | |
| sections.push(""); | |
| } | |
| } | |
| // Examples section | |
| if (doc.examples && doc.examples.length > 0) { | |
| sections.push(colors.bold("EXAMPLES")); | |
| let first = true; | |
| let previousHadDescription = false; | |
| for (const example of doc.examples) { | |
| if (example.description) { | |
| if (!first) sections.push(""); | |
| sections.push(` ${colors.dim(`# ${example.description}`)}`); | |
| } else if (previousHadDescription) { | |
| sections.push(""); | |
| } | |
| sections.push(` ${colors.cyan(example.command)}`); | |
| first = false; | |
| previousHadDescription = !!example.description; | |
| } | |
| sections.push(""); | |
| } | |
| // Remove trailing empty line if present | |
| if (sections[sections.length - 1] === "") { | |
| sections.pop(); | |
| } | |
| return sections.join("\n"); | |
| }; | |
| //# sourceMappingURL=CliOutput.js.map |
Xet Storage Details
- Size:
- 11.6 kB
- Xet hash:
- 5d9a660450fb0be4245348ff9be6f1acbfeb083833c7da165e4480d72afae9b3
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.