@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.
- package/.devcontainer/devcontainer.json +19 -0
- package/.devcontainer/install-prerequisites.sh +49 -0
- package/.github/workflows/copilot-setup-steps.yml +32 -0
- package/.github/workflows/pull-request.yml +27 -0
- package/.github/workflows/release-npm-package.yml +78 -0
- package/LICENSE +21 -0
- package/README.md +524 -0
- package/examples/tui-app/commands/greet.ts +75 -0
- package/examples/tui-app/commands/index.ts +3 -0
- package/examples/tui-app/commands/math.ts +114 -0
- package/examples/tui-app/commands/status.ts +75 -0
- package/examples/tui-app/index.ts +34 -0
- package/guides/01-hello-world.md +96 -0
- package/guides/02-adding-options.md +103 -0
- package/guides/03-multiple-commands.md +163 -0
- package/guides/04-subcommands.md +206 -0
- package/guides/05-interactive-tui.md +194 -0
- package/guides/06-config-validation.md +264 -0
- package/guides/07-async-cancellation.md +388 -0
- package/guides/08-complete-application.md +673 -0
- package/guides/README.md +74 -0
- package/package.json +32 -0
- package/src/__tests__/application.test.ts +425 -0
- package/src/__tests__/buildCliCommand.test.ts +125 -0
- package/src/__tests__/builtins.test.ts +133 -0
- package/src/__tests__/colors.test.ts +127 -0
- package/src/__tests__/command.test.ts +157 -0
- package/src/__tests__/commandClass.test.ts +130 -0
- package/src/__tests__/context.test.ts +97 -0
- package/src/__tests__/help.test.ts +412 -0
- package/src/__tests__/parser.test.ts +268 -0
- package/src/__tests__/registry.test.ts +195 -0
- package/src/__tests__/registryNew.test.ts +160 -0
- package/src/__tests__/schemaToFields.test.ts +176 -0
- package/src/__tests__/table.test.ts +146 -0
- package/src/__tests__/tui.test.ts +26 -0
- package/src/builtins/help.ts +85 -0
- package/src/builtins/index.ts +4 -0
- package/src/builtins/settings.ts +106 -0
- package/src/builtins/version.ts +72 -0
- package/src/cli/help.ts +174 -0
- package/src/cli/index.ts +3 -0
- package/src/cli/output/colors.ts +74 -0
- package/src/cli/output/index.ts +2 -0
- package/src/cli/output/table.ts +141 -0
- package/src/cli/parser.ts +241 -0
- package/src/commands/help.ts +50 -0
- package/src/commands/index.ts +1 -0
- package/src/components/index.ts +147 -0
- package/src/core/application.ts +461 -0
- package/src/core/command.ts +269 -0
- package/src/core/context.ts +112 -0
- package/src/core/help.ts +214 -0
- package/src/core/index.ts +15 -0
- package/src/core/logger.ts +164 -0
- package/src/core/registry.ts +140 -0
- package/src/hooks/index.ts +131 -0
- package/src/index.ts +137 -0
- package/src/registry/commandRegistry.ts +77 -0
- package/src/registry/index.ts +1 -0
- package/src/tui/TuiApp.tsx +582 -0
- package/src/tui/TuiApplication.tsx +230 -0
- package/src/tui/app.ts +29 -0
- package/src/tui/components/ActionButton.tsx +36 -0
- package/src/tui/components/CliModal.tsx +81 -0
- package/src/tui/components/CommandSelector.tsx +159 -0
- package/src/tui/components/ConfigForm.tsx +148 -0
- package/src/tui/components/EditorModal.tsx +177 -0
- package/src/tui/components/FieldRow.tsx +30 -0
- package/src/tui/components/Header.tsx +31 -0
- package/src/tui/components/JsonHighlight.tsx +128 -0
- package/src/tui/components/LogsPanel.tsx +86 -0
- package/src/tui/components/ResultsPanel.tsx +93 -0
- package/src/tui/components/StatusBar.tsx +59 -0
- package/src/tui/components/index.ts +13 -0
- package/src/tui/components/types.ts +30 -0
- package/src/tui/context/KeyboardContext.tsx +118 -0
- package/src/tui/context/index.ts +7 -0
- package/src/tui/hooks/index.ts +35 -0
- package/src/tui/hooks/useClipboard.ts +66 -0
- package/src/tui/hooks/useCommandExecutor.ts +131 -0
- package/src/tui/hooks/useConfigState.ts +171 -0
- package/src/tui/hooks/useKeyboardHandler.ts +91 -0
- package/src/tui/hooks/useLogStream.ts +96 -0
- package/src/tui/hooks/useSpinner.ts +46 -0
- package/src/tui/index.ts +65 -0
- package/src/tui/theme.ts +21 -0
- package/src/tui/utils/buildCliCommand.ts +90 -0
- package/src/tui/utils/index.ts +13 -0
- package/src/tui/utils/parameterPersistence.ts +96 -0
- package/src/tui/utils/schemaToFields.ts +144 -0
- package/src/types/command.ts +103 -0
- package/src/types/execution.ts +11 -0
- package/src/types/index.ts +1 -0
- 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
|
+
}
|
package/src/core/help.ts
ADDED
|
@@ -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";
|