@pablozaiden/terminatui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/.devcontainer/devcontainer.json +19 -0
  2. package/.devcontainer/install-prerequisites.sh +49 -0
  3. package/.github/workflows/copilot-setup-steps.yml +32 -0
  4. package/.github/workflows/pull-request.yml +27 -0
  5. package/.github/workflows/release-npm-package.yml +78 -0
  6. package/LICENSE +21 -0
  7. package/README.md +524 -0
  8. package/examples/tui-app/commands/greet.ts +75 -0
  9. package/examples/tui-app/commands/index.ts +3 -0
  10. package/examples/tui-app/commands/math.ts +114 -0
  11. package/examples/tui-app/commands/status.ts +75 -0
  12. package/examples/tui-app/index.ts +34 -0
  13. package/guides/01-hello-world.md +96 -0
  14. package/guides/02-adding-options.md +103 -0
  15. package/guides/03-multiple-commands.md +163 -0
  16. package/guides/04-subcommands.md +206 -0
  17. package/guides/05-interactive-tui.md +194 -0
  18. package/guides/06-config-validation.md +264 -0
  19. package/guides/07-async-cancellation.md +388 -0
  20. package/guides/08-complete-application.md +673 -0
  21. package/guides/README.md +74 -0
  22. package/package.json +32 -0
  23. package/src/__tests__/application.test.ts +425 -0
  24. package/src/__tests__/buildCliCommand.test.ts +125 -0
  25. package/src/__tests__/builtins.test.ts +133 -0
  26. package/src/__tests__/colors.test.ts +127 -0
  27. package/src/__tests__/command.test.ts +157 -0
  28. package/src/__tests__/commandClass.test.ts +130 -0
  29. package/src/__tests__/context.test.ts +97 -0
  30. package/src/__tests__/help.test.ts +412 -0
  31. package/src/__tests__/parser.test.ts +268 -0
  32. package/src/__tests__/registry.test.ts +195 -0
  33. package/src/__tests__/registryNew.test.ts +160 -0
  34. package/src/__tests__/schemaToFields.test.ts +176 -0
  35. package/src/__tests__/table.test.ts +146 -0
  36. package/src/__tests__/tui.test.ts +26 -0
  37. package/src/builtins/help.ts +85 -0
  38. package/src/builtins/index.ts +4 -0
  39. package/src/builtins/settings.ts +106 -0
  40. package/src/builtins/version.ts +72 -0
  41. package/src/cli/help.ts +174 -0
  42. package/src/cli/index.ts +3 -0
  43. package/src/cli/output/colors.ts +74 -0
  44. package/src/cli/output/index.ts +2 -0
  45. package/src/cli/output/table.ts +141 -0
  46. package/src/cli/parser.ts +241 -0
  47. package/src/commands/help.ts +50 -0
  48. package/src/commands/index.ts +1 -0
  49. package/src/components/index.ts +147 -0
  50. package/src/core/application.ts +461 -0
  51. package/src/core/command.ts +269 -0
  52. package/src/core/context.ts +112 -0
  53. package/src/core/help.ts +214 -0
  54. package/src/core/index.ts +15 -0
  55. package/src/core/logger.ts +164 -0
  56. package/src/core/registry.ts +140 -0
  57. package/src/hooks/index.ts +131 -0
  58. package/src/index.ts +137 -0
  59. package/src/registry/commandRegistry.ts +77 -0
  60. package/src/registry/index.ts +1 -0
  61. package/src/tui/TuiApp.tsx +582 -0
  62. package/src/tui/TuiApplication.tsx +230 -0
  63. package/src/tui/app.ts +29 -0
  64. package/src/tui/components/ActionButton.tsx +36 -0
  65. package/src/tui/components/CliModal.tsx +81 -0
  66. package/src/tui/components/CommandSelector.tsx +159 -0
  67. package/src/tui/components/ConfigForm.tsx +148 -0
  68. package/src/tui/components/EditorModal.tsx +177 -0
  69. package/src/tui/components/FieldRow.tsx +30 -0
  70. package/src/tui/components/Header.tsx +31 -0
  71. package/src/tui/components/JsonHighlight.tsx +128 -0
  72. package/src/tui/components/LogsPanel.tsx +86 -0
  73. package/src/tui/components/ResultsPanel.tsx +93 -0
  74. package/src/tui/components/StatusBar.tsx +59 -0
  75. package/src/tui/components/index.ts +13 -0
  76. package/src/tui/components/types.ts +30 -0
  77. package/src/tui/context/KeyboardContext.tsx +118 -0
  78. package/src/tui/context/index.ts +7 -0
  79. package/src/tui/hooks/index.ts +35 -0
  80. package/src/tui/hooks/useClipboard.ts +66 -0
  81. package/src/tui/hooks/useCommandExecutor.ts +131 -0
  82. package/src/tui/hooks/useConfigState.ts +171 -0
  83. package/src/tui/hooks/useKeyboardHandler.ts +91 -0
  84. package/src/tui/hooks/useLogStream.ts +96 -0
  85. package/src/tui/hooks/useSpinner.ts +46 -0
  86. package/src/tui/index.ts +65 -0
  87. package/src/tui/theme.ts +21 -0
  88. package/src/tui/utils/buildCliCommand.ts +90 -0
  89. package/src/tui/utils/index.ts +13 -0
  90. package/src/tui/utils/parameterPersistence.ts +96 -0
  91. package/src/tui/utils/schemaToFields.ts +144 -0
  92. package/src/types/command.ts +103 -0
  93. package/src/types/execution.ts +11 -0
  94. package/src/types/index.ts +1 -0
  95. package/tsconfig.json +25 -0
@@ -0,0 +1,85 @@
1
+ import { Command, type AnyCommand } from "../core/command.ts";
2
+ import type { AppContext } from "../core/context.ts";
3
+ import { generateCommandHelp, generateAppHelp } from "../core/help.ts";
4
+ import type { OptionSchema } from "../types/command.ts";
5
+
6
+ /**
7
+ * Built-in help command that is auto-injected as a subcommand into all commands.
8
+ * When invoked, it displays help for the parent command.
9
+ *
10
+ * This command is created internally by the Application class and should not
11
+ * be instantiated directly.
12
+ */
13
+ export class HelpCommand extends Command<OptionSchema> {
14
+ readonly name = "help";
15
+ readonly description = "Show help for this command";
16
+ readonly options = {} as const;
17
+
18
+ private parentCommand: AnyCommand | null = null;
19
+ private allCommands: AnyCommand[] = [];
20
+ private appName: string;
21
+ private appVersion: string;
22
+
23
+ constructor(config: {
24
+ parentCommand?: AnyCommand;
25
+ allCommands?: AnyCommand[];
26
+ appName: string;
27
+ appVersion: string;
28
+ }) {
29
+ super();
30
+ this.parentCommand = config.parentCommand ?? null;
31
+ this.allCommands = config.allCommands ?? [];
32
+ this.appName = config.appName;
33
+ this.appVersion = config.appVersion;
34
+ }
35
+
36
+ override async execute(_ctx: AppContext): Promise<void> {
37
+ let helpText: string;
38
+
39
+ if (this.parentCommand) {
40
+ // Show help for the parent command
41
+ helpText = generateCommandHelp(this.parentCommand, {
42
+ appName: this.appName,
43
+ version: this.appVersion,
44
+ });
45
+ } else {
46
+ // Show help for the entire application
47
+ helpText = generateAppHelp(this.allCommands, {
48
+ appName: this.appName,
49
+ version: this.appVersion,
50
+ });
51
+ }
52
+
53
+ console.log(helpText);
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Create a help command for a specific parent command.
59
+ */
60
+ export function createHelpCommandForParent(
61
+ parentCommand: AnyCommand,
62
+ appName: string,
63
+ appVersion: string
64
+ ): HelpCommand {
65
+ return new HelpCommand({
66
+ parentCommand,
67
+ appName,
68
+ appVersion,
69
+ });
70
+ }
71
+
72
+ /**
73
+ * Create a help command for the application root.
74
+ */
75
+ export function createRootHelpCommand(
76
+ allCommands: AnyCommand[],
77
+ appName: string,
78
+ appVersion: string
79
+ ): HelpCommand {
80
+ return new HelpCommand({
81
+ allCommands,
82
+ appName,
83
+ appVersion,
84
+ });
85
+ }
@@ -0,0 +1,4 @@
1
+ export { HelpCommand, createHelpCommandForParent, createRootHelpCommand } from "./help.ts";
2
+ export { VersionCommand, createVersionCommand, formatVersion } from "./version.ts";
3
+ export { SettingsCommand, createSettingsCommand } from "./settings.ts";
4
+ export type { VersionConfig } from "./version.ts";
@@ -0,0 +1,106 @@
1
+ import { Command } from "../core/command.ts";
2
+ import type { AppContext } from "../core/context.ts";
3
+ import { LogLevel } from "../core/logger.ts";
4
+ import type { OptionSchema, OptionValues } from "../types/command.ts";
5
+ import type { CommandResult } from "../core/command.ts";
6
+
7
+ /**
8
+ * Options schema for the settings command.
9
+ */
10
+ const settingsOptions = {
11
+ "log-level": {
12
+ type: "string",
13
+ description: "Minimum log level to emit",
14
+ default: "info",
15
+ enum: ["silly", "trace", "debug", "info", "warn", "error", "fatal"],
16
+ label: "Log Level",
17
+ order: 1,
18
+ },
19
+ "detailed-logs": {
20
+ type: "boolean",
21
+ description: "Include timestamp and level in log output",
22
+ default: false,
23
+ label: "Detailed Logs",
24
+ order: 2,
25
+ },
26
+ } as const satisfies OptionSchema;
27
+
28
+ type SettingsOptions = OptionValues<typeof settingsOptions>;
29
+
30
+ /**
31
+ * Parsed settings configuration.
32
+ */
33
+ interface SettingsConfig {
34
+ logLevel: LogLevel;
35
+ detailedLogs: boolean;
36
+ }
37
+
38
+ /**
39
+ * Map of string log level names to LogLevel enum values.
40
+ */
41
+ const logLevelMap: Record<string, LogLevel> = {
42
+ silly: LogLevel.Silly,
43
+ trace: LogLevel.Trace,
44
+ debug: LogLevel.Debug,
45
+ info: LogLevel.Info,
46
+ warn: LogLevel.Warn,
47
+ error: LogLevel.Error,
48
+ fatal: LogLevel.Fatal,
49
+ };
50
+
51
+ /**
52
+ * Parse a string log level to the LogLevel enum.
53
+ */
54
+ function parseLogLevel(value?: string): LogLevel {
55
+ if (!value) return LogLevel.Info;
56
+ const level = logLevelMap[value.toLowerCase()];
57
+ return level ?? LogLevel.Info;
58
+ }
59
+
60
+ /**
61
+ * Built-in settings command for configuring logging.
62
+ *
63
+ * This command allows users to configure the log level and detailed logging
64
+ * format at runtime. It's automatically registered by TuiApplication.
65
+ *
66
+ * In CLI mode, these settings are typically passed as global options:
67
+ * --log-level <level> and --detailed-logs
68
+ *
69
+ * In TUI mode, this command provides a UI for configuring these settings.
70
+ */
71
+ export class SettingsCommand extends Command<typeof settingsOptions, SettingsConfig> {
72
+ readonly name = "settings";
73
+ override readonly displayName = "Settings";
74
+ readonly description = "Configure logging level and output format";
75
+ readonly options = settingsOptions;
76
+
77
+ override readonly actionLabel = "Save Settings";
78
+ override readonly immediateExecution = false;
79
+
80
+ override buildConfig(_ctx: AppContext, opts: SettingsOptions): SettingsConfig {
81
+ const logLevel = parseLogLevel(opts["log-level"] as string | undefined);
82
+ const detailedLogs = Boolean(opts["detailed-logs"]);
83
+
84
+ return { logLevel, detailedLogs };
85
+ }
86
+
87
+ override async execute(ctx: AppContext, config: SettingsConfig): Promise<CommandResult> {
88
+ this.applySettings(ctx, config);
89
+ return {
90
+ success: true,
91
+ message: `Logging set to ${LogLevel[config.logLevel]}${config.detailedLogs ? " with detailed format" : ""}`,
92
+ };
93
+ }
94
+
95
+ private applySettings(ctx: AppContext, config: SettingsConfig): void {
96
+ ctx.logger.setMinLevel(config.logLevel);
97
+ ctx.logger.setDetailed(config.detailedLogs);
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Create the built-in settings command.
103
+ */
104
+ export function createSettingsCommand(): SettingsCommand {
105
+ return new SettingsCommand();
106
+ }
@@ -0,0 +1,72 @@
1
+ import { Command } from "../core/command.ts";
2
+ import type { AppContext } from "../core/context.ts";
3
+ import { colors } from "../cli/output/colors.ts";
4
+ import type { OptionSchema } from "../types/command.ts";
5
+
6
+ /**
7
+ * Configuration for version command.
8
+ */
9
+ export interface VersionConfig {
10
+ /** Application name */
11
+ appName: string;
12
+ /** Application version (e.g., "1.0.0") */
13
+ appVersion: string;
14
+ /** Optional commit hash for version display */
15
+ commitHash?: string;
16
+ }
17
+
18
+ /**
19
+ * Format version string with optional commit hash.
20
+ * If commitHash is empty or undefined, shows "(dev)".
21
+ */
22
+ export function formatVersion(version: string, commitHash?: string): string {
23
+ const hashPart = commitHash && commitHash.length > 0
24
+ ? commitHash.substring(0, 7)
25
+ : "(dev)";
26
+ return `${version} - ${hashPart}`;
27
+ }
28
+
29
+ /**
30
+ * Built-in version command that displays the application version.
31
+ * Automatically registered at the top level by the Application class.
32
+ */
33
+ export class VersionCommand extends Command<OptionSchema> {
34
+ readonly name = "version";
35
+ readonly description = "Show version information";
36
+ readonly options = {} as const;
37
+ readonly aliases = ["--version", "-v"];
38
+
39
+ private appName: string;
40
+ private appVersion: string;
41
+ private commitHash?: string;
42
+
43
+ constructor(config: VersionConfig) {
44
+ super();
45
+ this.appName = config.appName;
46
+ this.appVersion = config.appVersion;
47
+ this.commitHash = config.commitHash;
48
+ }
49
+
50
+ /**
51
+ * Get the formatted version string.
52
+ */
53
+ getFormattedVersion(): string {
54
+ return formatVersion(this.appVersion, this.commitHash);
55
+ }
56
+
57
+ override async execute(_ctx: AppContext): Promise<void> {
58
+ const versionDisplay = this.getFormattedVersion();
59
+ console.log(`${colors.bold(this.appName)} ${colors.dim(`v${versionDisplay}`)}`);
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Create a version command for the application.
65
+ */
66
+ export function createVersionCommand(
67
+ appName: string,
68
+ appVersion: string,
69
+ commitHash?: string
70
+ ): VersionCommand {
71
+ return new VersionCommand({ appName, appVersion, commitHash });
72
+ }
@@ -0,0 +1,174 @@
1
+ import type { Command, OptionDef } from "../types/command.ts";
2
+ import { colors } from "./output/colors.ts";
3
+
4
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
+ type AnyCommand = Command<any, any>;
6
+
7
+ /**
8
+ * Format usage line for a command
9
+ */
10
+ export function formatUsage(
11
+ command: AnyCommand,
12
+ appName = "cli"
13
+ ): string {
14
+ const parts = [appName, command.name];
15
+
16
+ if (command.subcommands && Object.keys(command.subcommands).length > 0) {
17
+ parts.push("[command]");
18
+ }
19
+
20
+ if (command.options && Object.keys(command.options).length > 0) {
21
+ parts.push("[options]");
22
+ }
23
+
24
+ return parts.join(" ");
25
+ }
26
+
27
+ /**
28
+ * Format subcommands list
29
+ */
30
+ export function formatCommands(command: AnyCommand): string {
31
+ if (!command.subcommands) return "";
32
+
33
+ const entries = Object.entries(command.subcommands)
34
+ .filter(([, cmd]) => !cmd.hidden)
35
+ .map(([name, cmd]) => {
36
+ const aliases = cmd.aliases?.length ? ` (${cmd.aliases.join(", ")})` : "";
37
+ return ` ${colors.cyan(name)}${aliases} ${cmd.description}`;
38
+ });
39
+
40
+ if (entries.length === 0) return "";
41
+
42
+ return ["Commands:", ...entries].join("\n");
43
+ }
44
+
45
+ /**
46
+ * Format options list
47
+ */
48
+ export function formatOptions(command: AnyCommand): string {
49
+ if (!command.options) return "";
50
+
51
+ const entries = Object.entries(command.options).map(([name, defUntyped]) => {
52
+ const def = defUntyped as OptionDef;
53
+ const alias = def.alias ? `-${def.alias}, ` : " ";
54
+ const flag = `${alias}--${name}`;
55
+ const required = def.required ? colors.red("*") : "";
56
+ const defaultVal = def.default !== undefined ? ` (default: ${def.default})` : "";
57
+ const enumVals = def.enum ? ` [${def.enum.join("|")}]` : "";
58
+
59
+ return ` ${colors.yellow(flag)}${required} ${def.description}${enumVals}${defaultVal}`;
60
+ });
61
+
62
+ if (entries.length === 0) return "";
63
+
64
+ return ["Options:", ...entries].join("\n");
65
+ }
66
+
67
+ /**
68
+ * Format examples list
69
+ */
70
+ export function formatExamples(command: AnyCommand): string {
71
+ if (!command.examples?.length) return "";
72
+
73
+ const entries = command.examples.map(
74
+ (ex) => ` ${colors.dim("$")} ${ex.command}\n ${colors.dim(ex.description)}`
75
+ );
76
+
77
+ return ["Examples:", ...entries].join("\n");
78
+ }
79
+
80
+ /**
81
+ * Format global options (log-level, detailed-logs)
82
+ */
83
+ export function formatGlobalOptions(): string {
84
+ const entries = [
85
+ ` ${colors.yellow("--log-level")} <level> Set log level [silly|trace|debug|info|warn|error|fatal]`,
86
+ ` ${colors.yellow("--detailed-logs")} Enable detailed log output`,
87
+ ` ${colors.yellow("--no-detailed-logs")} Disable detailed log output`,
88
+ ];
89
+
90
+ return ["Global Options:", ...entries].join("\n");
91
+ }
92
+
93
+ /**
94
+ * Get command summary line
95
+ */
96
+ export function getCommandSummary(command: AnyCommand): string {
97
+ const aliases = command.aliases?.length ? ` (${command.aliases.join(", ")})` : "";
98
+ return `${command.name}${aliases}: ${command.description}`;
99
+ }
100
+
101
+ /**
102
+ * Generate full help text for a command
103
+ */
104
+ export function generateHelp(
105
+ command: AnyCommand,
106
+ options: { appName?: string; version?: string } = {}
107
+ ): string {
108
+ const { appName = "cli", version } = options;
109
+ const sections: string[] = [];
110
+
111
+ // Header
112
+ if (version) {
113
+ sections.push(`${colors.bold(appName)} ${colors.dim(`v${version}`)}`);
114
+ }
115
+
116
+ // Description
117
+ sections.push(command.description);
118
+
119
+ // Usage
120
+ sections.push(`\n${colors.bold("Usage:")}\n ${formatUsage(command, appName)}`);
121
+
122
+ // Commands
123
+ const commandsSection = formatCommands(command);
124
+ if (commandsSection) {
125
+ sections.push(`\n${commandsSection}`);
126
+ }
127
+
128
+ // Options
129
+ const optionsSection = formatOptions(command);
130
+ if (optionsSection) {
131
+ sections.push(`\n${optionsSection}`);
132
+ }
133
+
134
+ // Examples
135
+ const examplesSection = formatExamples(command);
136
+ if (examplesSection) {
137
+ sections.push(`\n${examplesSection}`);
138
+ }
139
+
140
+ return sections.join("\n");
141
+ }
142
+
143
+ /**
144
+ * Generate help text for a specific command (includes global options)
145
+ */
146
+ export function generateCommandHelp(
147
+ command: AnyCommand,
148
+ appName = "cli"
149
+ ): string {
150
+ const sections: string[] = [];
151
+
152
+ // Description
153
+ sections.push(command.description);
154
+
155
+ // Usage
156
+ sections.push(`\n${colors.bold("Usage:")}\n ${formatUsage(command, appName)}`);
157
+
158
+ // Options
159
+ const optionsSection = formatOptions(command);
160
+ if (optionsSection) {
161
+ sections.push(`\n${optionsSection}`);
162
+ }
163
+
164
+ // Global Options
165
+ sections.push(`\n${formatGlobalOptions()}`);
166
+
167
+ // Examples
168
+ const examplesSection = formatExamples(command);
169
+ if (examplesSection) {
170
+ sections.push(`\n${examplesSection}`);
171
+ }
172
+
173
+ return sections.join("\n");
174
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./parser.ts";
2
+ export * from "./help.ts";
3
+ export * from "./output/index.ts";
@@ -0,0 +1,74 @@
1
+ // ANSI escape codes
2
+ const RESET = "\x1b[0m";
3
+ const BOLD = "\x1b[1m";
4
+ const DIM = "\x1b[2m";
5
+ const ITALIC = "\x1b[3m";
6
+ const UNDERLINE = "\x1b[4m";
7
+ const STRIKETHROUGH = "\x1b[9m";
8
+
9
+ const RED = "\x1b[31m";
10
+ const GREEN = "\x1b[32m";
11
+ const YELLOW = "\x1b[33m";
12
+ const BLUE = "\x1b[34m";
13
+ const MAGENTA = "\x1b[35m";
14
+ const CYAN = "\x1b[36m";
15
+ const WHITE = "\x1b[37m";
16
+ const GRAY = "\x1b[90m";
17
+
18
+ const BG_RED = "\x1b[41m";
19
+ const BG_GREEN = "\x1b[42m";
20
+ const BG_YELLOW = "\x1b[43m";
21
+ const BG_BLUE = "\x1b[44m";
22
+
23
+ /**
24
+ * Check if terminal supports colors
25
+ */
26
+ export function supportsColors(): boolean {
27
+ if (typeof process === "undefined") return false;
28
+ if (process.env["NO_COLOR"]) return false;
29
+ if (process.env["FORCE_COLOR"]) return true;
30
+ if (process.stdout?.isTTY) return true;
31
+ return false;
32
+ }
33
+
34
+ function wrap(code: string, text: string): string {
35
+ if (!supportsColors()) return text;
36
+ return `${code}${text}${RESET}`;
37
+ }
38
+
39
+ /**
40
+ * Color utilities for terminal output
41
+ */
42
+ export const colors = {
43
+ // Basic colors
44
+ red: (text: string) => wrap(RED, text),
45
+ green: (text: string) => wrap(GREEN, text),
46
+ yellow: (text: string) => wrap(YELLOW, text),
47
+ blue: (text: string) => wrap(BLUE, text),
48
+ magenta: (text: string) => wrap(MAGENTA, text),
49
+ cyan: (text: string) => wrap(CYAN, text),
50
+ white: (text: string) => wrap(WHITE, text),
51
+ gray: (text: string) => wrap(GRAY, text),
52
+
53
+ // Styles
54
+ bold: (text: string) => wrap(BOLD, text),
55
+ dim: (text: string) => wrap(DIM, text),
56
+ italic: (text: string) => wrap(ITALIC, text),
57
+ underline: (text: string) => wrap(UNDERLINE, text),
58
+ strikethrough: (text: string) => wrap(STRIKETHROUGH, text),
59
+
60
+ // Background colors
61
+ bgRed: (text: string) => wrap(BG_RED, text),
62
+ bgGreen: (text: string) => wrap(BG_GREEN, text),
63
+ bgYellow: (text: string) => wrap(BG_YELLOW, text),
64
+ bgBlue: (text: string) => wrap(BG_BLUE, text),
65
+
66
+ // Semantic colors
67
+ success: (text: string) => wrap(GREEN, `✓ ${text}`),
68
+ error: (text: string) => wrap(RED, `✗ ${text}`),
69
+ warning: (text: string) => wrap(YELLOW, `⚠ ${text}`),
70
+ info: (text: string) => wrap(BLUE, `ℹ ${text}`),
71
+
72
+ // Reset
73
+ reset: RESET,
74
+ };
@@ -0,0 +1,2 @@
1
+ export * from "./colors.ts";
2
+ export * from "./table.ts";
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Column configuration for tables
3
+ */
4
+ export interface ColumnConfig {
5
+ key: string;
6
+ header?: string;
7
+ width?: number;
8
+ align?: "left" | "right" | "center";
9
+ formatter?: (value: unknown) => string;
10
+ }
11
+
12
+ export interface TableOptions {
13
+ columns?: (string | ColumnConfig)[];
14
+ showHeaders?: boolean;
15
+ border?: boolean;
16
+ }
17
+
18
+ /**
19
+ * Create a formatted table string
20
+ */
21
+ export function table<T extends Record<string, unknown>>(
22
+ data: T[],
23
+ options: TableOptions = {}
24
+ ): string {
25
+ if (data.length === 0) return "";
26
+
27
+ const { showHeaders = true } = options;
28
+
29
+ // Determine columns
30
+ const columns: ColumnConfig[] = options.columns
31
+ ? options.columns.map((col) =>
32
+ typeof col === "string" ? { key: col, header: col } : col
33
+ )
34
+ : Object.keys(data[0] ?? {}).map((key) => ({ key, header: key }));
35
+
36
+ // Calculate column widths
37
+ const widths = columns.map((col) => {
38
+ const headerWidth = (col.header ?? col.key).length;
39
+ const maxDataWidth = Math.max(
40
+ ...data.map((row) => {
41
+ const value = row[col.key];
42
+ const formatted = col.formatter ? col.formatter(value) : String(value ?? "");
43
+ return formatted.length;
44
+ })
45
+ );
46
+ return col.width ?? Math.max(headerWidth, maxDataWidth);
47
+ });
48
+
49
+ // Format a row
50
+ const formatRow = (values: string[]): string => {
51
+ return values
52
+ .map((val, i) => {
53
+ const width = widths[i] ?? 10;
54
+ const col = columns[i];
55
+ const align = col?.align ?? "left";
56
+
57
+ if (align === "right") {
58
+ return val.padStart(width);
59
+ } else if (align === "center") {
60
+ const leftPad = Math.floor((width - val.length) / 2);
61
+ return val.padStart(leftPad + val.length).padEnd(width);
62
+ }
63
+ return val.padEnd(width);
64
+ })
65
+ .join(" ");
66
+ };
67
+
68
+ const rows: string[] = [];
69
+
70
+ // Add header
71
+ if (showHeaders) {
72
+ const headers = columns.map((col) => col.header ?? col.key);
73
+ rows.push(formatRow(headers));
74
+ rows.push(widths.map((w) => "-".repeat(w)).join(" "));
75
+ }
76
+
77
+ // Add data rows
78
+ for (const row of data) {
79
+ const values = columns.map((col) => {
80
+ const value = row[col.key];
81
+ return col.formatter ? col.formatter(value) : String(value ?? "");
82
+ });
83
+ rows.push(formatRow(values));
84
+ }
85
+
86
+ return rows.join("\n");
87
+ }
88
+
89
+ /**
90
+ * Create a key-value list
91
+ */
92
+ export function keyValueList(
93
+ data: Record<string, unknown>,
94
+ options: { separator?: string } = {}
95
+ ): string {
96
+ const { separator = ":" } = options;
97
+
98
+ const entries = Object.entries(data);
99
+ if (entries.length === 0) return "";
100
+
101
+ const maxKeyLength = Math.max(...entries.map(([key]) => key.length));
102
+
103
+ return entries
104
+ .map(([key, value]) => `${key.padEnd(maxKeyLength)}${separator} ${value}`)
105
+ .join("\n");
106
+ }
107
+
108
+ /**
109
+ * Create a bullet list
110
+ */
111
+ export function bulletList(
112
+ items: string[],
113
+ options: { bullet?: string; indent?: number } = {}
114
+ ): string {
115
+ const { bullet = "•", indent = 0 } = options;
116
+
117
+ if (items.length === 0) return "";
118
+
119
+ const prefix = " ".repeat(indent);
120
+ return items.map((item) => `${prefix}${bullet} ${item}`).join("\n");
121
+ }
122
+
123
+ /**
124
+ * Create a numbered list
125
+ */
126
+ export function numberedList(
127
+ items: string[],
128
+ options: { start?: number; indent?: number } = {}
129
+ ): string {
130
+ const { start = 1, indent = 0 } = options;
131
+
132
+ if (items.length === 0) return "";
133
+
134
+ const prefix = " ".repeat(indent);
135
+ const maxNum = start + items.length - 1;
136
+ const numWidth = String(maxNum).length;
137
+
138
+ return items
139
+ .map((item, i) => `${prefix}${String(start + i).padStart(numWidth)}. ${item}`)
140
+ .join("\n");
141
+ }