tuix
Version:
A performant TUI framework for Bun with JSX and reactive state management
403 lines (350 loc) • 11.4 kB
text/typescript
/**
* CLI Help System
*
* Auto-generated help screens and documentation
*/
import type { CLIConfig, CommandConfig } from "./types"
import { largeTextWithPalette } from "../components/LargeText"
import { styledText, text, vstack, hstack } from "../core/view"
import { style, Colors } from "../styling/index"
import { styledBox } from "../layout/box"
import { Borders } from "../styling/borders"
import type { View } from "../core/types"
export interface HelpOptions {
showBranding?: boolean
showExamples?: boolean
colorize?: boolean
width?: number
}
export class HelpGenerator {
constructor(private config: CLIConfig) {}
/**
* Generate help text for the CLI or a specific command
*/
generateHelp(commandPath?: string[], options: HelpOptions = {}): string {
const {
showBranding = true,
showExamples = true,
colorize = true,
width = 80
} = options
if (!commandPath || commandPath.length === 0) {
return this.generateGlobalHelp(options)
}
return this.generateCommandHelp(commandPath, options)
}
/**
* Generate a beautiful interactive help component
*/
generateHelpComponent(commandPath?: string[]): View {
if (!commandPath || commandPath.length === 0) {
return this.generateGlobalHelpComponent()
}
return this.generateCommandHelpComponent(commandPath)
}
/**
* Generate global help text
*/
private generateGlobalHelp(options: HelpOptions): string {
const lines: string[] = []
// Header
lines.push(`${this.config.name} v${this.config.version}`)
if (this.config.description) {
lines.push(this.config.description)
}
lines.push("")
// Usage
lines.push("USAGE:")
lines.push(` ${this.config.name} [OPTIONS] <COMMAND>`)
lines.push("")
// Global options
if (this.hasOptions(this.config.options)) {
lines.push("OPTIONS:")
this.addOptionsHelp(lines, this.config.options || {})
lines.push("")
}
// Commands
if (this.hasCommands(this.config.commands)) {
lines.push("COMMANDS:")
this.addCommandsHelp(lines, this.config.commands || {})
lines.push("")
}
// Examples
if (options.showExamples) {
lines.push("EXAMPLES:")
lines.push(` ${this.config.name} --help Show this help`)
lines.push(` ${this.config.name} --version Show version`)
const commands = Object.keys(this.config.commands || {})
if (commands.length > 0) {
lines.push(` ${this.config.name} ${commands[0]} --help Show command help`)
}
lines.push("")
}
return lines.join('\n')
}
/**
* Generate command-specific help text
*/
private generateCommandHelp(commandPath: string[], options: HelpOptions): string {
const commandConfig = this.getCommandConfig(commandPath)
if (!commandConfig) {
return `Unknown command: ${commandPath.join(' ')}`
}
const lines: string[] = []
// Header
lines.push(`${this.config.name} ${commandPath.join(' ')}`)
if (commandConfig.description) {
lines.push(commandConfig.description)
}
lines.push("")
// Usage
lines.push("USAGE:")
const usage = [this.config.name, ...commandPath]
if (this.hasOptions(commandConfig.options)) {
usage.push("[OPTIONS]")
}
if (this.hasArgs(commandConfig.args)) {
Object.keys(commandConfig.args || {}).forEach(arg => {
usage.push(`<${arg}>`)
})
}
if (this.hasCommands(commandConfig.commands)) {
usage.push("<COMMAND>")
}
lines.push(` ${usage.join(' ')}`)
lines.push("")
// Arguments
if (this.hasArgs(commandConfig.args)) {
lines.push("ARGUMENTS:")
this.addArgsHelp(lines, commandConfig.args || {})
lines.push("")
}
// Options
if (this.hasOptions(commandConfig.options)) {
lines.push("OPTIONS:")
this.addOptionsHelp(lines, commandConfig.options || {})
lines.push("")
}
// Subcommands
if (this.hasCommands(commandConfig.commands)) {
lines.push("COMMANDS:")
this.addCommandsHelp(lines, commandConfig.commands || {})
lines.push("")
}
// Aliases
if (commandConfig.aliases && commandConfig.aliases.length > 0) {
lines.push("ALIASES:")
lines.push(` ${commandConfig.aliases.join(', ')}`)
lines.push("")
}
return lines.join('\n')
}
/**
* Generate interactive global help component
*/
private generateGlobalHelpComponent(): View {
// Beautiful branded header
const header = vstack(
largeTextWithPalette(this.config.name, "neon", {
font: 'ansiShadow',
mode: 'outlined'
}),
text(""),
styledText(
this.config.description || "A CLI built with CLI-KIT",
style().foreground(Colors.brightCyan).italic()
),
styledText(
`v${this.config.version}`,
style().foreground(Colors.gray)
),
text("")
)
// Commands section
const commandsSection = this.hasCommands(this.config.commands)
? styledBox(
vstack(
styledText("Available Commands", style().foreground(Colors.yellow).bold()),
text(""),
...this.getCommandViews(this.config.commands || {})
),
{
border: Borders.Rounded,
padding: { top: 1, right: 2, bottom: 1, left: 2 },
style: style().foreground(Colors.white)
}
)
: text("")
// Options section
const optionsSection = this.hasOptions(this.config.options)
? styledBox(
vstack(
styledText("Global Options", style().foreground(Colors.yellow).bold()),
text(""),
...this.getOptionViews(this.config.options || {})
),
{
border: Borders.Rounded,
padding: { top: 1, right: 2, bottom: 1, left: 2 },
style: style().foreground(Colors.white)
}
)
: text("")
// Footer
const footer = styledText(
"Use --help with any command for more information",
style().foreground(Colors.gray).italic()
)
return vstack(
header,
commandsSection,
text(""),
optionsSection,
text(""),
footer
)
}
/**
* Generate interactive command help component
*/
private generateCommandHelpComponent(commandPath: string[]): View {
const commandConfig = this.getCommandConfig(commandPath)
if (!commandConfig) {
return styledText(
`Unknown command: ${commandPath.join(' ')}`,
style().foreground(Colors.red)
)
}
const header = vstack(
styledText(
`${this.config.name} ${commandPath.join(' ')}`,
style().foreground(Colors.brightCyan).bold()
),
styledText(
commandConfig.description || "",
style().foreground(Colors.white)
),
text("")
)
const sections: View[] = []
// Arguments
if (this.hasArgs(commandConfig.args)) {
sections.push(
styledBox(
vstack(
styledText("Arguments", style().foreground(Colors.yellow).bold()),
text(""),
...this.getArgViews(commandConfig.args || {})
),
{
border: Borders.Rounded,
padding: { top: 1, right: 2, bottom: 1, left: 2 }
}
)
)
}
// Options
if (this.hasOptions(commandConfig.options)) {
sections.push(
styledBox(
vstack(
styledText("Options", style().foreground(Colors.yellow).bold()),
text(""),
...this.getOptionViews(commandConfig.options || {})
),
{
border: Borders.Rounded,
padding: { top: 1, right: 2, bottom: 1, left: 2 }
}
)
)
}
// Subcommands
if (this.hasCommands(commandConfig.commands)) {
sections.push(
styledBox(
vstack(
styledText("Commands", style().foreground(Colors.yellow).bold()),
text(""),
...this.getCommandViews(commandConfig.commands || {})
),
{
border: Borders.Rounded,
padding: { top: 1, right: 2, bottom: 1, left: 2 }
}
)
)
}
return vstack(
header,
...sections.reduce((acc, section) => [...acc, section, text("")], [] as View[])
)
}
// Helper methods
private hasOptions(options?: Record<string, any>): boolean {
return Boolean(options && Object.keys(options).length > 0)
}
private hasCommands(commands?: Record<string, any>): boolean {
return Boolean(commands && Object.keys(commands).length > 0)
}
private hasArgs(args?: Record<string, any>): boolean {
return Boolean(args && Object.keys(args).length > 0)
}
private addOptionsHelp(lines: string[], options: Record<string, any>): void {
Object.entries(options).forEach(([name, schema]) => {
const description = (schema as any)._def?.description || ""
lines.push(` --${name.padEnd(20)} ${description}`)
})
}
private addCommandsHelp(lines: string[], commands: Record<string, CommandConfig>): void {
Object.entries(commands).forEach(([name, config]) => {
if (!config.hidden) {
lines.push(` ${name.padEnd(20)} ${config.description || ""}`)
}
})
}
private addArgsHelp(lines: string[], args: Record<string, any>): void {
Object.entries(args).forEach(([name, schema]) => {
const description = (schema as any)._def?.description || ""
lines.push(` ${name.padEnd(20)} ${description}`)
})
}
private getCommandViews(commands: Record<string, CommandConfig>): View[] {
return Object.entries(commands)
.filter(([, config]) => !config.hidden)
.map(([name, config]) =>
hstack(
styledText(name.padEnd(20), style().foreground(Colors.cyan)),
styledText(config.description || "", style().foreground(Colors.white))
)
)
}
private getOptionViews(options: Record<string, any>): View[] {
return Object.entries(options).map(([name, schema]) => {
const description = (schema as any)._def?.description || ""
return hstack(
styledText(`--${name}`.padEnd(20), style().foreground(Colors.yellow)),
styledText(description, style().foreground(Colors.white))
)
})
}
private getArgViews(args: Record<string, any>): View[] {
return Object.entries(args).map(([name, schema]) => {
const description = (schema as any)._def?.description || ""
return hstack(
styledText(`<${name}>`.padEnd(20), style().foreground(Colors.green)),
styledText(description, style().foreground(Colors.white))
)
})
}
private getCommandConfig(commandPath: string[]): CommandConfig | null {
let currentCommands = this.config.commands || {}
let currentConfig: CommandConfig | null = null
for (const command of commandPath) {
currentConfig = currentCommands[command] || null
if (!currentConfig) return null
currentCommands = currentConfig.commands || {}
}
return currentConfig
}
}