| 1 | import * as CliOutput from "effect/cli/CliOutput"; |
| 2 | import 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 | */ |
| 8 | const 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 | |
| 20 | const compactFlag = (flag: FlagDoc): FlagDoc => ({ |
| 21 | ...flag, |
| 22 | type: compactFlagType(flag.type), |
| 23 | }); |
| 24 | |
| 25 | const 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 | */ |
| 36 | const 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 | */ |
| 65 | export 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 | }; |