@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
package/README.md ADDED
@@ -0,0 +1,524 @@
1
+ # @pablozaiden/terminatui
2
+
3
+ A type-safe, class-based framework for building CLI and TUI applications in TypeScript.
4
+
5
+ ## Features
6
+
7
+ - **Type-safe CLI parsing** - Define options with schemas that provide full TypeScript types
8
+ - **Class-based architecture** - Extend `Command` and `Application` classes for structured apps
9
+ - **Unified execution** - Single `execute()` method handles both CLI and TUI modes
10
+ - **Auto-generated TUI** - Interactive terminal UI generated from command definitions
11
+ - **Built-in commands** - Automatic `help` and `version` commands
12
+ - **Nested subcommands** - Hierarchical command structures with path resolution
13
+ - **Lifecycle hooks** - `beforeExecute()` and `afterExecute()` hooks on commands
14
+ - **Service container** - `AppContext` provides dependency injection for services
15
+ - **Integrated logging** - Logger with TUI-aware output handling
16
+ - **Cancellation support** - AbortSignal-based cancellation for long-running commands
17
+ - **Config validation** - `buildConfig()` hook for transforming and validating options
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ bun add @pablozaiden/terminatui
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ### 1. Define a Command
28
+
29
+ ```typescript
30
+ import { Command, type AppContext, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
31
+
32
+ const greetOptions = {
33
+ name: {
34
+ type: "string",
35
+ description: "Name to greet",
36
+ required: true,
37
+ },
38
+ loud: {
39
+ type: "boolean",
40
+ description: "Use uppercase",
41
+ alias: "l",
42
+ default: false,
43
+ },
44
+ } satisfies OptionSchema;
45
+
46
+ class GreetCommand extends Command<typeof greetOptions> {
47
+ readonly name = "greet";
48
+ readonly description = "Greet someone";
49
+ readonly options = greetOptions;
50
+
51
+ override execute(ctx: AppContext, config: { name: string; loud: boolean }): CommandResult {
52
+ const message = `Hello, ${config.name}!`;
53
+ console.log(config.loud ? message.toUpperCase() : message);
54
+ return { success: true, message };
55
+ }
56
+ }
57
+ ```
58
+
59
+ ### 2. Create an Application
60
+
61
+ ```typescript
62
+ import { Application } from "@pablozaiden/terminatui";
63
+
64
+ class MyApp extends Application {
65
+ constructor() {
66
+ super({
67
+ name: "myapp",
68
+ version: "1.0.0",
69
+ description: "My awesome CLI app",
70
+ commands: [new GreetCommand()],
71
+ });
72
+ }
73
+ }
74
+ ```
75
+
76
+ ### 3. Run the Application
77
+
78
+ ```typescript
79
+ // index.ts
80
+ await new MyApp().run();
81
+ ```
82
+
83
+ ```bash
84
+ # Usage
85
+ myapp greet --name World
86
+ # Output: Hello, World!
87
+
88
+ myapp greet --name World --loud
89
+ # Output: HELLO, WORLD!
90
+
91
+ myapp help
92
+ # Shows all commands
93
+
94
+ myapp greet help
95
+ # Shows greet command options
96
+ ```
97
+
98
+ ## Core Concepts
99
+
100
+ ### Command
101
+
102
+ The `Command` abstract class is the base for all commands:
103
+
104
+ ```typescript
105
+ abstract class Command<TOptions extends OptionSchema = OptionSchema, TConfig = unknown> {
106
+ abstract readonly name: string;
107
+ abstract readonly description: string;
108
+ abstract readonly options: TOptions;
109
+
110
+ // Optional properties
111
+ displayName?: string; // Human-readable name for TUI
112
+ subCommands?: Command[]; // Nested subcommands
113
+ aliases?: string[]; // Command aliases
114
+ hidden?: boolean; // Hide from help
115
+ examples?: CommandExample[]; // Usage examples
116
+ longDescription?: string; // Extended description
117
+
118
+ // TUI customization
119
+ actionLabel?: string; // Button text (default: "Run")
120
+ immediateExecution?: boolean; // Execute on selection without config
121
+
122
+ // Required: Main execution method
123
+ abstract execute(
124
+ ctx: AppContext,
125
+ config: TConfig,
126
+ execCtx?: CommandExecutionContext
127
+ ): Promise<CommandResult | void> | CommandResult | void;
128
+
129
+ // Optional: Transform/validate options before execute
130
+ buildConfig?(ctx: AppContext, opts: OptionValues<TOptions>): TConfig | Promise<TConfig>;
131
+
132
+ // Optional: Custom result rendering for TUI
133
+ renderResult?(result: CommandResult): ReactNode;
134
+
135
+ // Optional: Custom clipboard content
136
+ getClipboardContent?(result: CommandResult): string | undefined;
137
+
138
+ // Optional: Handle config changes in TUI form
139
+ onConfigChange?(key: string, value: unknown, allValues: Record<string, unknown>): Record<string, unknown> | undefined;
140
+ }
141
+ ```
142
+
143
+ ### CommandExecutionContext
144
+
145
+ Provides execution context including cancellation support:
146
+
147
+ ```typescript
148
+ interface CommandExecutionContext {
149
+ signal: AbortSignal; // For cancellation
150
+ }
151
+ ```
152
+
153
+ ### CommandResult
154
+
155
+ Commands should return a `CommandResult` from `execute()`:
156
+
157
+ ```typescript
158
+ interface CommandResult {
159
+ success: boolean;
160
+ data?: unknown; // Result data
161
+ error?: string; // Error message if failed
162
+ message?: string; // User-friendly message
163
+ }
164
+ ```
165
+
166
+ ### Application
167
+
168
+ The `Application` class manages command registration and execution:
169
+
170
+ ```typescript
171
+ class Application {
172
+ constructor(config: ApplicationConfig);
173
+
174
+ run(args?: string[]): Promise<void>;
175
+ getContext(): AppContext;
176
+
177
+ // Lifecycle hooks (override in subclass)
178
+ onBeforeRun?(command: Command, options: Record<string, unknown>): void;
179
+ onAfterRun?(command: Command, result: unknown): void;
180
+ onError?(error: Error): void;
181
+ }
182
+
183
+ interface ApplicationConfig {
184
+ name: string;
185
+ version: string;
186
+ commitHash?: string; // Git commit for version display
187
+ description?: string;
188
+ commands: Command[];
189
+ defaultCommand?: string;
190
+ }
191
+ ```
192
+
193
+ ### AppContext
194
+
195
+ Access application-wide services and configuration:
196
+
197
+ ```typescript
198
+ import { AppContext } from "@pablozaiden/terminatui";
199
+
200
+ // Get the current context (set during Application.run())
201
+ const ctx = AppContext.current;
202
+
203
+ // Access the logger
204
+ ctx.logger.info("Hello");
205
+ ctx.logger.warn("Warning");
206
+ ctx.logger.error("Error");
207
+
208
+ // Access app config
209
+ console.log(ctx.config.name, ctx.config.version);
210
+
211
+ // Register and retrieve services
212
+ ctx.setService("myService", myServiceInstance);
213
+ const service = ctx.requireService<MyService>("myService");
214
+ ```
215
+
216
+ ### OptionSchema
217
+
218
+ Define typed options for commands:
219
+
220
+ ```typescript
221
+ interface OptionDef {
222
+ type: "string" | "boolean" | "number" | "array";
223
+ description: string;
224
+ required?: boolean;
225
+ default?: unknown;
226
+ alias?: string;
227
+ enum?: readonly string[]; // For string type, restrict to values
228
+
229
+ // TUI metadata
230
+ label?: string; // Custom label in form
231
+ order?: number; // Field ordering
232
+ group?: string; // Group fields together
233
+ placeholder?: string; // Placeholder text
234
+ tuiHidden?: boolean; // Hide from TUI form
235
+ }
236
+
237
+ type OptionSchema = Record<string, OptionDef>;
238
+ ```
239
+
240
+ ## Config Validation with buildConfig
241
+
242
+ Use `buildConfig()` to transform and validate options before execution:
243
+
244
+ ```typescript
245
+ import { Command, ConfigValidationError, type AppContext, type OptionValues } from "@pablozaiden/terminatui";
246
+
247
+ interface MyConfig {
248
+ resolvedPath: string;
249
+ count: number;
250
+ }
251
+
252
+ class MyCommand extends Command<typeof myOptions, MyConfig> {
253
+ readonly name = "mycommand";
254
+ readonly description = "Do something";
255
+ readonly options = myOptions;
256
+
257
+ override buildConfig(ctx: AppContext, opts: OptionValues<typeof myOptions>): MyConfig {
258
+ const pathRaw = opts["path"] as string | undefined;
259
+ if (!pathRaw) {
260
+ throw new ConfigValidationError("Missing required option: path", "path");
261
+ }
262
+
263
+ const count = parseInt(opts["count"] as string ?? "1", 10);
264
+ if (isNaN(count) || count <= 0) {
265
+ throw new ConfigValidationError("Count must be a positive integer", "count");
266
+ }
267
+
268
+ return {
269
+ resolvedPath: path.resolve(pathRaw),
270
+ count,
271
+ };
272
+ }
273
+
274
+ override async execute(ctx: AppContext, config: MyConfig): Promise<CommandResult> {
275
+ // config is now typed as MyConfig
276
+ ctx.logger.info(`Processing ${config.count} items from ${config.resolvedPath}`);
277
+ return { success: true };
278
+ }
279
+ }
280
+ ```
281
+
282
+ ## Cancellation Support
283
+
284
+ Commands can support cancellation via AbortSignal:
285
+
286
+ ```typescript
287
+ class LongRunningCommand extends Command<typeof options> {
288
+ // ...
289
+
290
+ override async execute(
291
+ ctx: AppContext,
292
+ config: Config,
293
+ execCtx?: CommandExecutionContext
294
+ ): Promise<CommandResult> {
295
+ for (const item of items) {
296
+ // Check for cancellation
297
+ if (execCtx?.signal.aborted) {
298
+ throw new AbortError("Command was cancelled");
299
+ }
300
+
301
+ await processItem(item, execCtx?.signal);
302
+ }
303
+
304
+ return { success: true };
305
+ }
306
+ }
307
+ ```
308
+
309
+ ## Subcommands
310
+
311
+ Commands can have nested subcommands:
312
+
313
+ ```typescript
314
+ class DbCommand extends Command {
315
+ name = "db";
316
+ description = "Database operations";
317
+
318
+ subCommands = [
319
+ new DbMigrateCommand(),
320
+ new DbSeedCommand(),
321
+ ];
322
+ }
323
+
324
+ // Usage: myapp db migrate
325
+ // myapp db seed
326
+ ```
327
+
328
+ ## Built-in Commands
329
+
330
+ The framework automatically injects:
331
+
332
+ - **`help`** - Shows command help (injected into every command as subcommand)
333
+ - **`version`** - Shows app version (top-level only)
334
+
335
+ ```bash
336
+ myapp help # App-level help
337
+ myapp greet help # Command-level help
338
+ myapp version # Shows version
339
+ ```
340
+
341
+ ## TUI Mode
342
+
343
+ Terminatui provides built-in TUI (Terminal User Interface) support that automatically generates interactive UIs from your command definitions.
344
+
345
+ ### TuiApplication
346
+
347
+ Extend `TuiApplication` instead of `Application` to get automatic TUI support:
348
+
349
+ ```typescript
350
+ import { TuiApplication, Command } from "@pablozaiden/terminatui";
351
+
352
+ class MyApp extends TuiApplication {
353
+ constructor() {
354
+ super({
355
+ name: "myapp",
356
+ displayName: "🚀 My App", // Human-readable name for TUI header
357
+ version: "1.0.0",
358
+ commands: [new RunCommand(), new ConfigCommand()],
359
+ enableTui: true, // default: true
360
+ });
361
+ }
362
+ }
363
+ ```
364
+
365
+ When run with no arguments, the app launches an interactive TUI instead of showing help:
366
+
367
+ ```bash
368
+ myapp # Launches TUI
369
+ myapp run --verbose # Runs in CLI mode
370
+ ```
371
+
372
+ ### TUI Metadata
373
+
374
+ Add TUI-specific metadata to your option schemas to customize the UI:
375
+
376
+ ```typescript
377
+ const myOptions = {
378
+ repo: {
379
+ type: "string",
380
+ description: "Repository path",
381
+ required: true,
382
+ // TUI metadata
383
+ label: "Repository", // Custom label in form
384
+ order: 1, // Field ordering
385
+ group: "Required", // Group fields together
386
+ placeholder: "/path", // Placeholder text
387
+ tuiHidden: false, // Hide from TUI form
388
+ },
389
+ verbose: {
390
+ type: "boolean",
391
+ description: "Verbose output",
392
+ label: "Verbose Mode",
393
+ order: 10,
394
+ group: "Options",
395
+ },
396
+ } satisfies OptionSchema;
397
+ ```
398
+
399
+ ### Command TUI Properties
400
+
401
+ Commands can customize their TUI behavior:
402
+
403
+ ```typescript
404
+ class RunCommand extends Command<typeof runOptions, RunConfig> {
405
+ readonly name = "run";
406
+ override readonly displayName = "Run Task"; // Shown in command selector
407
+ readonly description = "Run the task";
408
+ readonly options = runOptions;
409
+
410
+ // TUI customization
411
+ override readonly actionLabel = "Start Run"; // Button text
412
+ override readonly immediateExecution = false; // Run immediately on selection
413
+
414
+ // Return structured results for display
415
+ override async execute(ctx: AppContext, config: RunConfig): Promise<CommandResult> {
416
+ const result = await runTask(config);
417
+ return {
418
+ success: true,
419
+ data: result,
420
+ message: "Task completed"
421
+ };
422
+ }
423
+
424
+ // Custom result rendering (React/TSX)
425
+ override renderResult(result: CommandResult): ReactNode {
426
+ return <MyCustomResultView data={result.data} />;
427
+ }
428
+
429
+ // Content for clipboard (Ctrl+Y in results view)
430
+ override getClipboardContent(result: CommandResult): string | undefined {
431
+ return JSON.stringify(result.data, null, 2);
432
+ }
433
+
434
+ // React to config changes in the TUI form
435
+ override onConfigChange(
436
+ key: string,
437
+ value: unknown,
438
+ allValues: Record<string, unknown>
439
+ ): Record<string, unknown> | undefined {
440
+ if (key === "preset" && value === "fast") {
441
+ return { iterations: 1, parallel: true };
442
+ }
443
+ return undefined;
444
+ }
445
+ }
446
+ ```
447
+
448
+ ### TUI Features
449
+
450
+ The built-in TUI provides:
451
+
452
+ - **Command Selector** - Navigate and select commands with arrow keys
453
+ - **Config Form** - Auto-generated forms from option schemas with field groups
454
+ - **Field Editor** - Edit field values (text, number, boolean, enum)
455
+ - **CLI Modal** - View equivalent CLI command (press `C`)
456
+ - **Results Panel** - Display command results with custom rendering
457
+ - **Logs Panel** - View application logs in real-time
458
+ - **Clipboard Support** - Centralized copy with Ctrl+Y
459
+ - **Cancellation** - Cancel running commands with Esc
460
+ - **Parameter Persistence** - Remembers last-used values per command
461
+
462
+ ### Keyboard Shortcuts
463
+
464
+ | Key | Action |
465
+ |-----|--------|
466
+ | ↑/↓ | Navigate fields/commands |
467
+ | Enter | Edit field / Execute command |
468
+ | Tab | Cycle focus between panels |
469
+ | C | Show CLI command modal |
470
+ | L | Toggle logs panel |
471
+ | Ctrl+Y | Copy current content to clipboard |
472
+ | Esc | Back / Cancel running command |
473
+
474
+ ### TUI Utilities
475
+
476
+ The package exports utilities for building custom TUI components:
477
+
478
+ ```typescript
479
+ import {
480
+ // Form utilities
481
+ schemaToFieldConfigs, // Convert OptionSchema to form fields
482
+ getFieldDisplayValue, // Format field values for display
483
+ buildCliCommand, // Build CLI command from config
484
+
485
+ // Hooks
486
+ useKeyboardHandler, // Register keyboard handlers with priority
487
+ useClipboard, // Clipboard operations with OSC 52
488
+ useLogStream, // Stream logs from logger
489
+ useSpinner, // Animated spinner
490
+ useCommandExecutor, // Execute commands with cancellation
491
+
492
+ // Context
493
+ KeyboardProvider, // Keyboard context provider
494
+ KeyboardPriority, // Global < Focused < Modal
495
+
496
+ // Components
497
+ Theme, // TUI color theme
498
+ JsonHighlight, // Syntax-highlighted JSON display
499
+ } from "@pablozaiden/terminatui";
500
+ ```
501
+
502
+ ## Output Formatting
503
+
504
+ Terminatui includes utilities for formatted CLI output:
505
+
506
+ ```typescript
507
+ import { colors, table, bulletList, keyValueList } from "@pablozaiden/terminatui";
508
+
509
+ // Colors
510
+ console.log(colors.red("Error!"));
511
+ console.log(colors.success("Done!")); // ✓ Done!
512
+ console.log(colors.bold(colors.blue("Title")));
513
+
514
+ // Tables
515
+ console.log(table(data, ["name", "value", "status"]));
516
+
517
+ // Lists
518
+ console.log(bulletList(["Item 1", "Item 2", "Item 3"]));
519
+ console.log(keyValueList({ name: "Test", count: 42 }));
520
+ ```
521
+
522
+ ## License
523
+
524
+ MIT
@@ -0,0 +1,75 @@
1
+ import {
2
+ Command,
3
+ type AppContext,
4
+ type OptionSchema,
5
+ type OptionValues,
6
+ type CommandResult
7
+ } from "../../../src/index.ts";
8
+
9
+ const greetOptions = {
10
+ name: {
11
+ type: "string",
12
+ description: "Name to greet",
13
+ required: true,
14
+ label: "Name",
15
+ order: 1,
16
+ group: "Required",
17
+ placeholder: "Enter name...",
18
+ },
19
+ loud: {
20
+ type: "boolean",
21
+ description: "Use uppercase",
22
+ alias: "l",
23
+ default: false,
24
+ label: "Loud Mode",
25
+ order: 2,
26
+ group: "Options",
27
+ },
28
+ times: {
29
+ type: "number",
30
+ description: "Number of times to greet",
31
+ default: 1,
32
+ label: "Repeat Count",
33
+ order: 3,
34
+ group: "Options",
35
+ },
36
+ } as const satisfies OptionSchema;
37
+
38
+ export class GreetCommand extends Command<typeof greetOptions> {
39
+ readonly name = "greet";
40
+ readonly description = "Greet someone with a friendly message";
41
+ readonly options = greetOptions;
42
+
43
+ override readonly actionLabel = "Say Hello";
44
+
45
+ override readonly examples = [
46
+ { command: "greet --name World", description: "Simple greeting" },
47
+ { command: "greet --name World --loud --times 3", description: "Loud greeting 3 times" },
48
+ ];
49
+
50
+ override async execute(ctx: AppContext, opts: OptionValues<typeof greetOptions>): Promise<CommandResult> {
51
+ const greeting = this.createGreeting(opts);
52
+ ctx.logger.info(greeting);
53
+ return {
54
+ success: true,
55
+ data: { greeting, timestamp: new Date().toISOString() },
56
+ message: greeting,
57
+ };
58
+ }
59
+
60
+ override getClipboardContent(result: CommandResult): string | undefined {
61
+ const data = result.data as { greeting?: string } | undefined;
62
+ return data?.greeting;
63
+ }
64
+
65
+ private createGreeting(opts: OptionValues<typeof greetOptions>): string {
66
+ const name = opts.name as string;
67
+ const loud = opts.loud as boolean;
68
+ const times = (opts.times as number) || 1;
69
+
70
+ let message = `Hello, ${name}!`;
71
+ if (loud) message = message.toUpperCase();
72
+
73
+ return Array(times).fill(message).join("\n");
74
+ }
75
+ }
@@ -0,0 +1,3 @@
1
+ export { GreetCommand } from "./greet.ts";
2
+ export { MathCommand } from "./math.ts";
3
+ export { StatusCommand } from "./status.ts";
@@ -0,0 +1,114 @@
1
+ import {
2
+ Command,
3
+ type AppContext,
4
+ type OptionSchema,
5
+ type OptionValues,
6
+ type CommandResult
7
+ } from "../../../src/index.ts";
8
+
9
+ const mathOptions = {
10
+ operation: {
11
+ type: "string",
12
+ description: "Math operation to perform",
13
+ required: true,
14
+ enum: ["add", "subtract", "multiply", "divide"] as const,
15
+ label: "Operation",
16
+ order: 1,
17
+ group: "Required",
18
+ },
19
+ a: {
20
+ type: "number",
21
+ description: "First number",
22
+ required: true,
23
+ label: "First Number",
24
+ order: 2,
25
+ group: "Required",
26
+ },
27
+ b: {
28
+ type: "number",
29
+ description: "Second number",
30
+ required: true,
31
+ label: "Second Number",
32
+ order: 3,
33
+ group: "Required",
34
+ },
35
+ showSteps: {
36
+ type: "boolean",
37
+ description: "Show calculation steps",
38
+ default: false,
39
+ label: "Show Steps",
40
+ order: 4,
41
+ group: "Options",
42
+ },
43
+ } as const satisfies OptionSchema;
44
+
45
+ export class MathCommand extends Command<typeof mathOptions> {
46
+ readonly name = "math";
47
+ readonly description = "Perform basic math operations";
48
+ readonly options = mathOptions;
49
+
50
+ override readonly actionLabel = "Calculate";
51
+
52
+ override async execute(ctx: AppContext, opts: OptionValues<typeof mathOptions>): Promise<CommandResult> {
53
+ const result = this.calculate(opts);
54
+ if (!result.success) {
55
+ ctx.logger.error(result.message || "Calculation failed");
56
+ }
57
+ return result;
58
+ }
59
+
60
+ override renderResult(result: CommandResult): string {
61
+ if (!result.success) return result.message || "Error";
62
+ const data = result.data as { expression: string; result: number; steps?: string[] };
63
+ let output = `${data.expression} = ${data.result}`;
64
+ if (data.steps) {
65
+ output += "\n\nSteps:\n" + data.steps.map((s, i) => ` ${i + 1}. ${s}`).join("\n");
66
+ }
67
+ return output;
68
+ }
69
+
70
+ private calculate(opts: OptionValues<typeof mathOptions>): CommandResult {
71
+ const op = opts.operation as string;
72
+ const a = opts.a as number;
73
+ const b = opts.b as number;
74
+ const showSteps = opts.showSteps as boolean;
75
+
76
+ let result: number;
77
+ let expression: string;
78
+ const steps: string[] = [];
79
+
80
+ switch (op) {
81
+ case "add":
82
+ result = a + b;
83
+ expression = `${a} + ${b}`;
84
+ if (showSteps) steps.push(`Adding ${a} and ${b}`, `Result: ${result}`);
85
+ break;
86
+ case "subtract":
87
+ result = a - b;
88
+ expression = `${a} - ${b}`;
89
+ if (showSteps) steps.push(`Subtracting ${b} from ${a}`, `Result: ${result}`);
90
+ break;
91
+ case "multiply":
92
+ result = a * b;
93
+ expression = `${a} × ${b}`;
94
+ if (showSteps) steps.push(`Multiplying ${a} by ${b}`, `Result: ${result}`);
95
+ break;
96
+ case "divide":
97
+ if (b === 0) {
98
+ return { success: false, message: "Cannot divide by zero" };
99
+ }
100
+ result = a / b;
101
+ expression = `${a} ÷ ${b}`;
102
+ if (showSteps) steps.push(`Dividing ${a} by ${b}`, `Result: ${result}`);
103
+ break;
104
+ default:
105
+ return { success: false, message: `Unknown operation: ${op}` };
106
+ }
107
+
108
+ return {
109
+ success: true,
110
+ data: { expression, result, steps: showSteps ? steps : undefined },
111
+ message: `${expression} = ${result}`,
112
+ };
113
+ }
114
+ }