1import * as CliOutput from "effect/cli/CliOutput";
2import type { FlagDoc, HelpDoc } from "effect/cli/HelpDoc";
3
4/**
5 * Compact enum-style flag types so one long choice list (`--log-level`'s
6 * nine choices) doesn't widen the description column for every flag.
7 */
8const compactFlagType = (type: string): string => {
9 if (type === "boolean" || type.length <= 24) return type;
10 const bracketed = type.startsWith("<") && type.endsWith(">");
11 const value = bracketed ? type.slice(1, -1) : type;
12 const choices = value.split("|");
13 const compact =
14 choices.length > 2
15 ? `${choices.slice(0, 2).join("|")}|…`
16 : `${value.slice(0, 21)}…`;
17 return bracketed ? `<${compact}>` : compact;
18};
19
20const compactFlag = (flag: FlagDoc): FlagDoc => ({
21 ...flag,
22 type: compactFlagType(flag.type),
23});
24
25const compactDoc = (doc: HelpDoc): HelpDoc => ({
26 ...doc,
27 flags: doc.flags?.map(compactFlag),
28 globalFlags: doc.globalFlags?.map(compactFlag),
29});
30
31/**
32 * Bound every line to the terminal width, breaking at spaces; continuation
33 * lines keep the original indent plus two columns. The default formatter
34 * never wraps, so long descriptions would otherwise run off the terminal.
35 */
36const wrapLines = (text: string, width: number): string =>
37 text
38 .split("\n")
39 .flatMap((line) => {
40 if (line.length <= width) return [line];
41 const continuation = `${line.match(/^\s*/)![0]} `;
42 const out: string[] = [];
43 let current: string | undefined;
44 for (const word of line.trim().split(/\s+/)) {
45 if (current === undefined) {
46 current = `${line.match(/^\s*/)![0]}${word}`;
47 } else if (current.length + word.length + 1 <= width) {
48 current += ` ${word}`;
49 } else {
50 out.push(current);
51 current = continuation + word;
52 }
53 }
54 if (current !== undefined) out.push(current);
55 return out;
56 })
57 .join("\n");
58
59/**
60 * Help formatter for plain (non-interactive) runs: the default effect/cli
61 * formatter with colors off, enum flag types compacted, and output wrapped
62 * to the terminal width — CI logs, redirected output, and coding agents
63 * get parseable, bounded lines.
64 */
65export const plainCliFormatter = (options: {
66 columns: number;
67}): CliOutput.Formatter => {
68 const columns = Math.max(60, Math.min(options.columns, 120));
69 const fallback = CliOutput.defaultFormatter({ colors: false });
70 return {
71 ...fallback,
72 formatHelpDoc: (doc) =>
73 wrapLines(fallback.formatHelpDoc(compactDoc(doc)), columns),
74 };
75};