@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,269 @@
1
+ import type { ReactNode } from "react";
2
+ import type { AppContext } from "./context.ts";
3
+ import type { OptionSchema, OptionValues } from "../types/command.ts";
4
+
5
+ /**
6
+ * Example for command help documentation.
7
+ */
8
+ export interface CommandExample {
9
+ /** The command invocation */
10
+ command: string;
11
+ /** Description of what the example does */
12
+ description: string;
13
+ }
14
+
15
+ /**
16
+ * Result of command execution for TUI display.
17
+ */
18
+ export interface CommandResult {
19
+ /** Whether the command succeeded */
20
+ success: boolean;
21
+ /** Result data to display */
22
+ data?: unknown;
23
+ /** Error message if failed */
24
+ error?: string;
25
+ /** Summary message */
26
+ message?: string;
27
+ }
28
+
29
+ /**
30
+ * Context passed to command execute methods.
31
+ * Includes the abort signal for cancellation support.
32
+ */
33
+ export interface CommandExecutionContext {
34
+ /** Signal to check for cancellation */
35
+ signal: AbortSignal;
36
+ }
37
+
38
+ /**
39
+ * Error thrown when a command is aborted/cancelled.
40
+ */
41
+ export class AbortError extends Error {
42
+ constructor(message = "Command was cancelled") {
43
+ super(message);
44
+ this.name = "AbortError";
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Error thrown when configuration validation fails in buildConfig.
50
+ * This provides a structured way to report validation errors.
51
+ */
52
+ export class ConfigValidationError extends Error {
53
+ constructor(
54
+ message: string,
55
+ public readonly field?: string,
56
+ public readonly details?: Record<string, unknown>
57
+ ) {
58
+ super(message);
59
+ this.name = "ConfigValidationError";
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Type alias for any command regardless of its options or config types.
65
+ * Use this when storing commands in collections or passing them around
66
+ * without caring about the specific type parameters.
67
+ */
68
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
69
+ export type AnyCommand = Command<any, any>;
70
+
71
+ /**
72
+ * Abstract base class for commands.
73
+ *
74
+ * Extend this class to create commands that can run in CLI mode, TUI mode, or both.
75
+ * The framework enforces that at least one execute method is implemented.
76
+ *
77
+ * Commands can optionally implement `buildConfig` to transform and validate parsed
78
+ * options into a typed configuration object before execution.
79
+ *
80
+ * @typeParam TOptions - The option schema type (defines what CLI flags are accepted)
81
+ * @typeParam TConfig - The configuration type passed to execute methods. Defaults to
82
+ * OptionValues<TOptions> if buildConfig is not implemented.
83
+ *
84
+ * @example
85
+ * ```typescript
86
+ * interface RunConfig {
87
+ * repoPath: string;
88
+ * iterations: number;
89
+ * }
90
+ *
91
+ * class RunCommand extends Command<typeof runOptions, RunConfig> {
92
+ * name = "run";
93
+ * description = "Run the application";
94
+ * options = runOptions;
95
+ *
96
+ * async buildConfig(ctx: AppContext, opts: OptionValues<typeof runOptions>): Promise<RunConfig> {
97
+ * const repoPath = path.resolve(opts.repo);
98
+ * if (!existsSync(repoPath)) {
99
+ * throw new ConfigValidationError(`Repository not found: ${repoPath}`, "repo");
100
+ * }
101
+ * return { repoPath, iterations: parseInt(opts.iterations) };
102
+ * }
103
+ *
104
+ * async execute(ctx: AppContext, config: RunConfig) {
105
+ * // config is already validated
106
+ * return { success: true, data: result };
107
+ * }
108
+ * }
109
+ * ```
110
+ */
111
+ export abstract class Command<
112
+ TOptions extends OptionSchema = OptionSchema,
113
+ TConfig = OptionValues<TOptions>
114
+ > {
115
+ /** Command name used in CLI */
116
+ abstract readonly name: string;
117
+
118
+ /** Display name for TUI (human-readable, e.g., "Run Evaluation") */
119
+ displayName?: string;
120
+
121
+ /** Short description shown in help */
122
+ abstract readonly description: string;
123
+
124
+ /** Option schema defining accepted arguments */
125
+ abstract readonly options: TOptions;
126
+
127
+ /** Nested subcommands */
128
+ subCommands?: Command[];
129
+
130
+ /** Example usages for help text */
131
+ examples?: CommandExample[];
132
+
133
+ /** Extended description for detailed help */
134
+ longDescription?: string;
135
+
136
+ // TUI-specific properties
137
+
138
+ /** Label for the action button (e.g., "Run", "Generate", "Save") */
139
+ actionLabel?: string;
140
+
141
+ /** Whether this command runs immediately without config screen (like "check") */
142
+ immediateExecution?: boolean;
143
+
144
+ /**
145
+ * Build and validate a configuration object from parsed options.
146
+ *
147
+ * Override this method to transform raw CLI options into a typed configuration
148
+ * object, and perform any validation that requires the parsed values (e.g.,
149
+ * checking that a directory exists, validating combinations of options).
150
+ *
151
+ * If not overridden, the parsed options are passed directly to execute methods.
152
+ *
153
+ * @throws ConfigValidationError if validation fails
154
+ * @returns The validated configuration object
155
+ */
156
+ buildConfig?(ctx: AppContext, opts: OptionValues<TOptions>): Promise<TConfig> | TConfig;
157
+
158
+ /**
159
+ * Execute the command.
160
+ * The framework will call this method for both CLI and TUI modes.
161
+ *
162
+ * @param ctx - Application context
163
+ * @param config - The configuration object (from buildConfig, or raw options if buildConfig is not implemented)
164
+ * @param execCtx - Execution context with abort signal for cancellation support
165
+ * @returns Optional result for display in TUI results panel
166
+ */
167
+ abstract execute(ctx: AppContext, config: TConfig, execCtx?: CommandExecutionContext): Promise<CommandResult | void> | CommandResult | void;
168
+
169
+ /**
170
+ * Called before buildConfig. Use for early validation, resource acquisition, etc.
171
+ * If this throws, buildConfig and execute will not be called but afterExecute will still run.
172
+ */
173
+ beforeExecute?(ctx: AppContext, opts: OptionValues<TOptions>): Promise<void> | void;
174
+
175
+ /**
176
+ * Called after execute, even if execute threw an error.
177
+ * Use for cleanup, logging, etc.
178
+ * @param error The error thrown by beforeExecute, buildConfig, or execute, if any
179
+ */
180
+ afterExecute?(
181
+ ctx: AppContext,
182
+ opts: OptionValues<TOptions>,
183
+ error?: Error
184
+ ): Promise<void> | void;
185
+
186
+ /**
187
+ * Custom result renderer for TUI.
188
+ * If not provided, results are displayed as JSON.
189
+ */
190
+ renderResult?(result: CommandResult): ReactNode;
191
+
192
+ /**
193
+ * Get content to copy to clipboard.
194
+ * Called when user presses Ctrl+Y in results panel.
195
+ * Return undefined if nothing should be copied.
196
+ */
197
+ getClipboardContent?(result: CommandResult): string | undefined;
198
+
199
+ /**
200
+ * Called when a config value changes in the TUI.
201
+ *
202
+ * Override this to update related fields when one field changes.
203
+ * For example, changing "agent" could automatically update "model"
204
+ * to the default model for that agent.
205
+ *
206
+ * @param key - The key of the field that changed
207
+ * @param value - The new value
208
+ * @param allValues - All current config values (including the new value)
209
+ * @returns Updated values to merge, or undefined if no changes needed
210
+ *
211
+ * @example
212
+ * ```typescript
213
+ * onConfigChange(key: string, value: unknown, allValues: Record<string, unknown>) {
214
+ * if (key === "agent") {
215
+ * return { model: getDefaultModelForAgent(value as string) };
216
+ * }
217
+ * return undefined;
218
+ * }
219
+ * ```
220
+ */
221
+ onConfigChange?(
222
+ key: string,
223
+ value: unknown,
224
+ allValues: Record<string, unknown>
225
+ ): Record<string, unknown> | undefined;
226
+
227
+ /**
228
+ * Check if this command supports CLI mode.
229
+ */
230
+ supportsCli(): boolean {
231
+ return true;
232
+ }
233
+
234
+ /**
235
+ * Check if this command supports TUI mode.
236
+ */
237
+ supportsTui(): boolean {
238
+ return true;
239
+ }
240
+
241
+ /**
242
+ * Check if this command implements buildConfig.
243
+ */
244
+ hasConfig(): boolean {
245
+ return typeof this.buildConfig === "function";
246
+ }
247
+
248
+ /**
249
+ * Validate the command.
250
+ * Called by the framework during registration.
251
+ */
252
+ validate(): void {
253
+ // No validation needed - execute is abstract and required
254
+ }
255
+
256
+ /**
257
+ * Get a subcommand by name.
258
+ */
259
+ getSubCommand(name: string): Command | undefined {
260
+ return this.subCommands?.find((cmd) => cmd.name === name);
261
+ }
262
+
263
+ /**
264
+ * Check if this command has subcommands.
265
+ */
266
+ hasSubCommands(): boolean {
267
+ return (this.subCommands?.length ?? 0) > 0;
268
+ }
269
+ }
@@ -0,0 +1,112 @@
1
+ import { Logger, createLogger, type LoggerConfig } from "./logger.ts";
2
+
3
+ /**
4
+ * Application configuration stored in context.
5
+ */
6
+ export interface AppConfig {
7
+ /** Application name */
8
+ name: string;
9
+ /** Application version */
10
+ version: string;
11
+ /** Additional configuration values */
12
+ [key: string]: unknown;
13
+ }
14
+
15
+ /**
16
+ * AppContext is the central container for application-wide services and state.
17
+ * It holds the logger, configuration, and a generic service registry.
18
+ *
19
+ * Access the current context via `AppContext.current` or receive it
20
+ * as a parameter in command execute methods.
21
+ */
22
+ export class AppContext {
23
+ private static _current: AppContext | null = null;
24
+ private readonly services = new Map<string, unknown>();
25
+
26
+ /** The application logger */
27
+ public readonly logger: Logger;
28
+
29
+ /** The application configuration */
30
+ public readonly config: AppConfig;
31
+
32
+ constructor(config: AppConfig, loggerConfig?: LoggerConfig) {
33
+ this.config = config;
34
+ this.logger = createLogger(loggerConfig);
35
+ }
36
+
37
+ /**
38
+ * Get the current application context.
39
+ * Throws if no context has been set.
40
+ */
41
+ static get current(): AppContext {
42
+ if (!AppContext._current) {
43
+ throw new Error(
44
+ "AppContext.current accessed before initialization. " +
45
+ "Ensure Application.run() has been called."
46
+ );
47
+ }
48
+ return AppContext._current;
49
+ }
50
+
51
+ /**
52
+ * Check if a current context exists.
53
+ */
54
+ static hasCurrent(): boolean {
55
+ return AppContext._current !== null;
56
+ }
57
+
58
+ /**
59
+ * Set the current application context.
60
+ * Called internally by Application.
61
+ */
62
+ static setCurrent(context: AppContext): void {
63
+ AppContext._current = context;
64
+ }
65
+
66
+ /**
67
+ * Clear the current context.
68
+ * Useful for testing.
69
+ */
70
+ static clearCurrent(): void {
71
+ AppContext._current = null;
72
+ }
73
+
74
+ /**
75
+ * Register a service in the context.
76
+ * @param name Unique service identifier
77
+ * @param service The service instance
78
+ */
79
+ setService<T>(name: string, service: T): void {
80
+ this.services.set(name, service);
81
+ }
82
+
83
+ /**
84
+ * Get a service from the context.
85
+ * @param name Service identifier
86
+ * @returns The service instance or undefined
87
+ */
88
+ getService<T>(name: string): T | undefined {
89
+ return this.services.get(name) as T | undefined;
90
+ }
91
+
92
+ /**
93
+ * Get a service, throwing if not found.
94
+ * @param name Service identifier
95
+ * @returns The service instance
96
+ */
97
+ requireService<T>(name: string): T {
98
+ const service = this.getService<T>(name);
99
+ if (service === undefined) {
100
+ throw new Error(`Service '${name}' not found in AppContext`);
101
+ }
102
+ return service;
103
+ }
104
+
105
+ /**
106
+ * Check if a service is registered.
107
+ * @param name Service identifier
108
+ */
109
+ hasService(name: string): boolean {
110
+ return this.services.has(name);
111
+ }
112
+ }
@@ -0,0 +1,214 @@
1
+ import { type AnyCommand } from "./command.ts";
2
+ import type { OptionDef } from "../types/command.ts";
3
+ import { colors } from "../cli/output/colors.ts";
4
+
5
+ /**
6
+ * Options for generating help text.
7
+ */
8
+ export interface HelpOptions {
9
+ /** Application name (used in usage line) */
10
+ appName?: string;
11
+ /** Application version (shown in header) */
12
+ version?: string;
13
+ /** Command path leading to this command (e.g., ["app", "remote", "add"]) */
14
+ commandPath?: string[];
15
+ }
16
+
17
+ /**
18
+ * Format the usage line for a command.
19
+ */
20
+ export function formatUsage(command: AnyCommand, options: HelpOptions = {}): string {
21
+ const { appName = "cli", commandPath = [] } = options;
22
+
23
+ const parts = [appName, ...commandPath];
24
+
25
+ // Add command name if not already in path
26
+ if (commandPath.length === 0 || commandPath[commandPath.length - 1] !== command.name) {
27
+ parts.push(command.name);
28
+ }
29
+
30
+ if (command.hasSubCommands()) {
31
+ parts.push("[command]");
32
+ }
33
+
34
+ if (command.options && Object.keys(command.options).length > 0) {
35
+ parts.push("[options]");
36
+ }
37
+
38
+ return parts.join(" ");
39
+ }
40
+
41
+ /**
42
+ * Format subcommands list.
43
+ */
44
+ export function formatSubCommands(command: AnyCommand): string {
45
+ if (!command.subCommands?.length) return "";
46
+
47
+ const entries = command.subCommands.map((cmd) => {
48
+ const modes: string[] = [];
49
+ if (cmd.supportsCli()) modes.push("cli");
50
+ if (cmd.supportsTui()) modes.push("tui");
51
+ const modeHint = modes.length ? colors.dim(` [${modes.join("/")}]`) : "";
52
+
53
+ return ` ${colors.cyan(cmd.name)}${modeHint} ${cmd.description}`;
54
+ });
55
+
56
+ if (entries.length === 0) return "";
57
+
58
+ return [colors.bold("Commands:"), ...entries].join("\n");
59
+ }
60
+
61
+ /**
62
+ * Format options list.
63
+ */
64
+ export function formatOptions(command: AnyCommand): string {
65
+ if (!command.options || Object.keys(command.options).length === 0) return "";
66
+
67
+ const entries = Object.entries(command.options).map(([name, defUntyped]) => {
68
+ const def = defUntyped as OptionDef;
69
+ const alias = def.alias ? `-${def.alias}, ` : " ";
70
+ const flag = `${alias}--${name}`;
71
+ const required = def.required ? colors.red(" (required)") : "";
72
+ const defaultVal =
73
+ def.default !== undefined ? colors.dim(` [default: ${def.default}]`) : "";
74
+ const enumVals = def.enum ? colors.dim(` [${def.enum.join(" | ")}]`) : "";
75
+ const typeHint = colors.dim(` <${def.type}>`);
76
+
77
+ return ` ${colors.yellow(flag)}${typeHint}${required}\n ${def.description}${enumVals}${defaultVal}`;
78
+ });
79
+
80
+ return [colors.bold("Options:"), ...entries].join("\n");
81
+ }
82
+
83
+ /**
84
+ * Format global options section (available on all commands).
85
+ */
86
+ export function formatGlobalOptions(): string {
87
+ const entries = [
88
+ ` ${colors.yellow(" --log-level")}${colors.dim(" <string>")}\n Set minimum log level${colors.dim(" [silly | trace | debug | info | warn | error | fatal]")}`,
89
+ ` ${colors.yellow(" --detailed-logs")}\n Include timestamp and level prefix in log output`,
90
+ ` ${colors.yellow(" --no-detailed-logs")}\n Disable detailed log format`,
91
+ ];
92
+
93
+ return [colors.bold("Global Options:"), ...entries].join("\n");
94
+ }
95
+
96
+ /**
97
+ * Format examples list.
98
+ */
99
+ export function formatExamples(command: AnyCommand): string {
100
+ if (!command.examples?.length) return "";
101
+
102
+ const entries = command.examples.map(
103
+ (ex) => ` ${colors.dim("$")} ${ex.command}\n ${colors.dim(ex.description)}`
104
+ );
105
+
106
+ return [colors.bold("Examples:"), ...entries].join("\n");
107
+ }
108
+
109
+ /**
110
+ * Generate full help text for a command.
111
+ *
112
+ * @param command The command to generate help for
113
+ * @param options Help generation options
114
+ * @returns Formatted help text
115
+ */
116
+ export function generateCommandHelp(command: AnyCommand, options: HelpOptions = {}): string {
117
+ const { appName = "cli", version } = options;
118
+ const sections: string[] = [];
119
+
120
+ // Header with version
121
+ if (version) {
122
+ sections.push(`${colors.bold(appName)} ${colors.dim(`v${version}`)}\n`);
123
+ }
124
+
125
+ // Description
126
+ sections.push(command.description);
127
+
128
+ // Long description if available
129
+ if (command.longDescription) {
130
+ sections.push(`\n${command.longDescription}`);
131
+ }
132
+
133
+ // Execution modes
134
+ const modes: string[] = [];
135
+ if (command.supportsCli()) modes.push("CLI");
136
+ if (command.supportsTui()) modes.push("TUI");
137
+ if (modes.length > 0) {
138
+ sections.push(`\n${colors.dim(`Supports: ${modes.join(", ")}`)}`);
139
+ }
140
+
141
+ // Usage
142
+ sections.push(`\n${colors.bold("Usage:")}\n ${formatUsage(command, options)}`);
143
+
144
+ // Subcommands
145
+ const subCommandsSection = formatSubCommands(command);
146
+ if (subCommandsSection) {
147
+ sections.push(`\n${subCommandsSection}`);
148
+ }
149
+
150
+ // Options
151
+ const optionsSection = formatOptions(command);
152
+ if (optionsSection) {
153
+ sections.push(`\n${optionsSection}`);
154
+ }
155
+
156
+ // Global options (available on all commands)
157
+ sections.push(`\n${formatGlobalOptions()}`);
158
+
159
+ // Examples
160
+ const examplesSection = formatExamples(command);
161
+ if (examplesSection) {
162
+ sections.push(`\n${examplesSection}`);
163
+ }
164
+
165
+ // Help hint
166
+ if (command.hasSubCommands()) {
167
+ sections.push(
168
+ `\n${colors.dim(`Run '${appName} ${command.name} <command> help' for more information on a command.`)}`
169
+ );
170
+ }
171
+
172
+ return sections.join("\n");
173
+ }
174
+
175
+ /**
176
+ * Generate help text for the application root (list of all commands).
177
+ *
178
+ * @param commands List of top-level commands
179
+ * @param options Help generation options
180
+ * @returns Formatted help text
181
+ */
182
+ export function generateAppHelp(commands: AnyCommand[], options: HelpOptions = {}): string {
183
+ const { appName = "cli", version } = options;
184
+ const sections: string[] = [];
185
+
186
+ // Header
187
+ if (version) {
188
+ sections.push(`${colors.bold(appName)} ${colors.dim(`v${version}`)}\n`);
189
+ }
190
+
191
+ // Usage
192
+ sections.push(`${colors.bold("Usage:")}\n ${appName} [command] [options]\n`);
193
+
194
+ // Commands
195
+ if (commands.length > 0) {
196
+ const entries = commands.map((cmd) => {
197
+ const modes: string[] = [];
198
+ if (cmd.supportsCli()) modes.push("cli");
199
+ if (cmd.supportsTui()) modes.push("tui");
200
+ const modeHint = modes.length ? colors.dim(` [${modes.join("/")}]`) : "";
201
+
202
+ return ` ${colors.cyan(cmd.name)}${modeHint} ${cmd.description}`;
203
+ });
204
+
205
+ sections.push([colors.bold("Commands:"), ...entries].join("\n"));
206
+ }
207
+
208
+ // Help hint
209
+ sections.push(
210
+ `\n${colors.dim(`Run '${appName} <command> help' for more information on a command.`)}`
211
+ );
212
+
213
+ return sections.join("\n");
214
+ }
@@ -0,0 +1,15 @@
1
+ // Core exports
2
+ export { Application, type ApplicationConfig, type ApplicationHooks, type GlobalOptions } from "./application.ts";
3
+ export { AppContext, type AppConfig } from "./context.ts";
4
+ export { Command, ConfigValidationError, AbortError, type AnyCommand, type CommandExample, type CommandResult, type CommandExecutionContext } from "./command.ts";
5
+ export { CommandRegistry, type ResolveResult } from "./registry.ts";
6
+ export { Logger, createLogger, LogLevel, type LoggerConfig, type LogEvent } from "./logger.ts";
7
+ export {
8
+ generateCommandHelp,
9
+ generateAppHelp,
10
+ formatUsage,
11
+ formatSubCommands,
12
+ formatOptions,
13
+ formatExamples,
14
+ type HelpOptions,
15
+ } from "./help.ts";