@pablozaiden/terminatui 0.2.0 → 0.3.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/README.md +64 -43
- package/package.json +11 -8
- package/src/__tests__/application.test.ts +87 -68
- package/src/__tests__/buildCliCommand.test.ts +99 -119
- package/src/__tests__/builtins.test.ts +27 -75
- package/src/__tests__/command.test.ts +100 -131
- package/src/__tests__/configOnChange.test.ts +63 -0
- package/src/__tests__/context.test.ts +1 -26
- package/src/__tests__/helpCore.test.ts +227 -0
- package/src/__tests__/parser.test.ts +98 -244
- package/src/__tests__/registry.test.ts +33 -160
- package/src/__tests__/schemaToFields.test.ts +75 -158
- package/src/builtins/help.ts +12 -4
- package/src/builtins/settings.ts +18 -32
- package/src/builtins/version.ts +3 -3
- package/src/cli/output/colors.ts +1 -1
- package/src/cli/parser.ts +26 -95
- package/src/core/application.ts +192 -110
- package/src/core/command.ts +26 -9
- package/src/core/context.ts +31 -20
- package/src/core/help.ts +24 -18
- package/src/core/knownCommands.ts +13 -0
- package/src/core/logger.ts +39 -42
- package/src/core/registry.ts +5 -12
- package/src/index.ts +22 -137
- package/src/tui/TuiApplication.tsx +63 -120
- package/src/tui/TuiRoot.tsx +135 -0
- package/src/tui/adapters/factory.ts +19 -0
- package/src/tui/adapters/ink/InkRenderer.tsx +139 -0
- package/src/tui/adapters/ink/components/Button.tsx +12 -0
- package/src/tui/adapters/ink/components/Code.tsx +6 -0
- package/src/tui/adapters/ink/components/CodeHighlight.tsx +6 -0
- package/src/tui/adapters/ink/components/Container.tsx +5 -0
- package/src/tui/adapters/ink/components/Field.tsx +12 -0
- package/src/tui/adapters/ink/components/Label.tsx +24 -0
- package/src/tui/adapters/ink/components/MenuButton.tsx +12 -0
- package/src/tui/adapters/ink/components/MenuItem.tsx +17 -0
- package/src/tui/adapters/ink/components/Overlay.tsx +5 -0
- package/src/tui/adapters/ink/components/Panel.tsx +15 -0
- package/src/tui/adapters/ink/components/ScrollView.tsx +5 -0
- package/src/tui/adapters/ink/components/Select.tsx +44 -0
- package/src/tui/adapters/ink/components/Spacer.tsx +15 -0
- package/src/tui/adapters/ink/components/Spinner.tsx +5 -0
- package/src/tui/adapters/ink/components/TextInput.tsx +22 -0
- package/src/tui/adapters/ink/components/Value.tsx +7 -0
- package/src/tui/adapters/ink/keyboard.ts +97 -0
- package/src/tui/adapters/ink/utils.ts +16 -0
- package/src/tui/adapters/opentui/OpenTuiRenderer.tsx +119 -0
- package/src/tui/adapters/opentui/components/Button.tsx +13 -0
- package/src/tui/adapters/opentui/components/Code.tsx +12 -0
- package/src/tui/adapters/opentui/components/CodeHighlight.tsx +24 -0
- package/src/tui/adapters/opentui/components/Container.tsx +56 -0
- package/src/tui/adapters/opentui/components/Field.tsx +18 -0
- package/src/tui/adapters/opentui/components/Label.tsx +15 -0
- package/src/tui/adapters/opentui/components/MenuButton.tsx +14 -0
- package/src/tui/adapters/opentui/components/MenuItem.tsx +29 -0
- package/src/tui/adapters/opentui/components/Overlay.tsx +21 -0
- package/src/tui/adapters/opentui/components/Panel.tsx +78 -0
- package/src/tui/adapters/opentui/components/ScrollView.tsx +85 -0
- package/src/tui/adapters/opentui/components/Select.tsx +59 -0
- package/src/tui/adapters/opentui/components/Spacer.tsx +5 -0
- package/src/tui/adapters/opentui/components/Spinner.tsx +12 -0
- package/src/tui/adapters/opentui/components/TextInput.tsx +13 -0
- package/src/tui/adapters/opentui/components/Value.tsx +13 -0
- package/src/tui/{hooks → adapters/opentui/hooks}/useSpinner.ts +2 -11
- package/src/tui/adapters/opentui/keyboard.ts +61 -0
- package/src/tui/adapters/types.ts +71 -0
- package/src/tui/components/ActionButton.tsx +0 -36
- package/src/tui/components/CommandSelector.tsx +45 -92
- package/src/tui/components/ConfigForm.tsx +68 -42
- package/src/tui/components/FieldRow.tsx +0 -30
- package/src/tui/components/Header.tsx +14 -13
- package/src/tui/components/JsonHighlight.tsx +10 -17
- package/src/tui/components/ModalBase.tsx +38 -0
- package/src/tui/components/ResultsPanel.tsx +27 -36
- package/src/tui/components/StatusBar.tsx +24 -39
- package/src/tui/components/logColors.ts +12 -0
- package/src/tui/context/ClipboardContext.tsx +87 -0
- package/src/tui/context/ExecutorContext.tsx +139 -0
- package/src/tui/context/KeyboardContext.tsx +85 -71
- package/src/tui/context/LogsContext.tsx +35 -0
- package/src/tui/context/NavigationContext.tsx +194 -0
- package/src/tui/context/RendererContext.tsx +20 -0
- package/src/tui/context/TuiAppContext.tsx +58 -0
- package/src/tui/hooks/useActiveKeyHandler.ts +75 -0
- package/src/tui/hooks/useBackHandler.ts +34 -0
- package/src/tui/hooks/useClipboard.ts +40 -25
- package/src/tui/hooks/useClipboardProvider.ts +42 -0
- package/src/tui/hooks/useGlobalKeyHandler.ts +54 -0
- package/src/tui/modals/CliModal.tsx +82 -0
- package/src/tui/modals/EditorModal.tsx +207 -0
- package/src/tui/modals/LogsModal.tsx +98 -0
- package/src/tui/registry.ts +102 -0
- package/src/tui/screens/CommandSelectScreen.tsx +162 -0
- package/src/tui/screens/ConfigScreen.tsx +165 -0
- package/src/tui/screens/ErrorScreen.tsx +58 -0
- package/src/tui/screens/ResultsScreen.tsx +68 -0
- package/src/tui/screens/RunningScreen.tsx +72 -0
- package/src/tui/screens/ScreenBase.ts +6 -0
- package/src/tui/semantic/Button.tsx +7 -0
- package/src/tui/semantic/Code.tsx +7 -0
- package/src/tui/semantic/CodeHighlight.tsx +7 -0
- package/src/tui/semantic/Container.tsx +7 -0
- package/src/tui/semantic/Field.tsx +7 -0
- package/src/tui/semantic/Label.tsx +7 -0
- package/src/tui/semantic/MenuButton.tsx +7 -0
- package/src/tui/semantic/MenuItem.tsx +7 -0
- package/src/tui/semantic/Overlay.tsx +7 -0
- package/src/tui/semantic/Panel.tsx +7 -0
- package/src/tui/semantic/ScrollView.tsx +9 -0
- package/src/tui/semantic/Select.tsx +7 -0
- package/src/tui/semantic/Spacer.tsx +7 -0
- package/src/tui/semantic/Spinner.tsx +7 -0
- package/src/tui/semantic/TextInput.tsx +7 -0
- package/src/tui/semantic/Value.tsx +7 -0
- package/src/tui/semantic/types.ts +195 -0
- package/src/tui/theme.ts +25 -14
- package/src/tui/utils/buildCliCommand.ts +1 -0
- package/src/tui/utils/getEnumKeys.ts +3 -0
- package/src/tui/utils/parameterPersistence.ts +1 -0
- package/src/types/command.ts +0 -60
- package/.devcontainer/devcontainer.json +0 -19
- package/.devcontainer/install-prerequisites.sh +0 -49
- package/.github/workflows/copilot-setup-steps.yml +0 -32
- package/.github/workflows/pull-request.yml +0 -27
- package/.github/workflows/release-npm-package.yml +0 -81
- package/AGENTS.md +0 -31
- package/bun.lock +0 -236
- package/examples/tui-app/commands/config/app/get.ts +0 -66
- package/examples/tui-app/commands/config/app/index.ts +0 -27
- package/examples/tui-app/commands/config/app/set.ts +0 -86
- package/examples/tui-app/commands/config/index.ts +0 -32
- package/examples/tui-app/commands/config/user/get.ts +0 -65
- package/examples/tui-app/commands/config/user/index.ts +0 -27
- package/examples/tui-app/commands/config/user/set.ts +0 -61
- package/examples/tui-app/commands/greet.ts +0 -76
- package/examples/tui-app/commands/index.ts +0 -4
- package/examples/tui-app/commands/math.ts +0 -115
- package/examples/tui-app/commands/status.ts +0 -77
- package/examples/tui-app/index.ts +0 -35
- package/guides/01-hello-world.md +0 -96
- package/guides/02-adding-options.md +0 -103
- package/guides/03-multiple-commands.md +0 -163
- package/guides/04-subcommands.md +0 -206
- package/guides/05-interactive-tui.md +0 -194
- package/guides/06-config-validation.md +0 -264
- package/guides/07-async-cancellation.md +0 -336
- package/guides/08-complete-application.md +0 -537
- package/guides/README.md +0 -74
- package/src/__tests__/colors.test.ts +0 -127
- package/src/__tests__/commandClass.test.ts +0 -130
- package/src/__tests__/help.test.ts +0 -412
- package/src/__tests__/registryNew.test.ts +0 -160
- package/src/__tests__/table.test.ts +0 -146
- package/src/__tests__/tui.test.ts +0 -26
- package/src/builtins/index.ts +0 -4
- package/src/cli/help.ts +0 -174
- package/src/cli/index.ts +0 -3
- package/src/cli/output/index.ts +0 -2
- package/src/cli/output/table.ts +0 -141
- package/src/commands/help.ts +0 -50
- package/src/commands/index.ts +0 -1
- package/src/components/index.ts +0 -147
- package/src/core/index.ts +0 -15
- package/src/hooks/index.ts +0 -131
- package/src/registry/commandRegistry.ts +0 -77
- package/src/registry/index.ts +0 -1
- package/src/tui/TuiApp.tsx +0 -619
- package/src/tui/app.ts +0 -29
- package/src/tui/components/CliModal.tsx +0 -81
- package/src/tui/components/EditorModal.tsx +0 -177
- package/src/tui/components/LogsPanel.tsx +0 -86
- package/src/tui/components/index.ts +0 -13
- package/src/tui/context/index.ts +0 -7
- package/src/tui/hooks/index.ts +0 -35
- package/src/tui/hooks/useKeyboardHandler.ts +0 -91
- package/src/tui/hooks/useLogStream.ts +0 -96
- package/src/tui/index.ts +0 -65
- package/src/tui/utils/index.ts +0 -13
- package/src/types/index.ts +0 -1
- package/tsconfig.json +0 -25
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ A type-safe, class-based framework for building CLI and TUI applications in Type
|
|
|
12
12
|
- **Nested subcommands** - Hierarchical command structures with path resolution
|
|
13
13
|
- **Lifecycle hooks** - `beforeExecute()` and `afterExecute()` hooks on commands
|
|
14
14
|
- **Service container** - `AppContext` provides dependency injection for services
|
|
15
|
-
- **Integrated logging** - Logger with TUI-aware output handling
|
|
15
|
+
- **Integrated logging** - Logger with TUI-aware output handling (live log modal with global copy shortcut)
|
|
16
16
|
- **Cancellation support** - AbortSignal-based cancellation for long-running commands
|
|
17
17
|
- **Config validation** - `buildConfig()` hook for transforming and validating options
|
|
18
18
|
|
|
@@ -27,7 +27,12 @@ bun add @pablozaiden/terminatui
|
|
|
27
27
|
### 1. Define a Command
|
|
28
28
|
|
|
29
29
|
```typescript
|
|
30
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
Command,
|
|
32
|
+
type OptionSchema,
|
|
33
|
+
type CommandResult,
|
|
34
|
+
type CommandExecutionContext,
|
|
35
|
+
} from "@pablozaiden/terminatui";
|
|
31
36
|
|
|
32
37
|
const greetOptions = {
|
|
33
38
|
name: {
|
|
@@ -48,7 +53,10 @@ class GreetCommand extends Command<typeof greetOptions> {
|
|
|
48
53
|
readonly description = "Greet someone";
|
|
49
54
|
readonly options = greetOptions;
|
|
50
55
|
|
|
51
|
-
override execute(
|
|
56
|
+
override execute(
|
|
57
|
+
config: { name: string; loud: boolean },
|
|
58
|
+
_execCtx: CommandExecutionContext
|
|
59
|
+
): CommandResult {
|
|
52
60
|
const message = `Hello, ${config.name}!`;
|
|
53
61
|
console.log(config.loud ? message.toUpperCase() : message);
|
|
54
62
|
return { success: true, message };
|
|
@@ -77,7 +85,11 @@ class MyApp extends Application {
|
|
|
77
85
|
|
|
78
86
|
```typescript
|
|
79
87
|
// index.ts
|
|
88
|
+
// Recommended: let Terminatui read `Bun.argv.slice(2)`
|
|
80
89
|
await new MyApp().run();
|
|
90
|
+
|
|
91
|
+
// For tests or programmatic invocation:
|
|
92
|
+
// await new MyApp().runFromArgs(["greet", "--name", "World"]);
|
|
81
93
|
```
|
|
82
94
|
|
|
83
95
|
```bash
|
|
@@ -121,9 +133,8 @@ abstract class Command<TOptions extends OptionSchema = OptionSchema, TConfig = u
|
|
|
121
133
|
|
|
122
134
|
// Required: Main execution method
|
|
123
135
|
abstract execute(
|
|
124
|
-
ctx: AppContext,
|
|
125
136
|
config: TConfig,
|
|
126
|
-
execCtx
|
|
137
|
+
execCtx: CommandExecutionContext
|
|
127
138
|
): Promise<CommandResult | void> | CommandResult | void;
|
|
128
139
|
|
|
129
140
|
// Optional: Transform/validate options before execute
|
|
@@ -170,10 +181,15 @@ The `Application` class manages command registration and execution:
|
|
|
170
181
|
```typescript
|
|
171
182
|
class Application {
|
|
172
183
|
constructor(config: ApplicationConfig);
|
|
173
|
-
|
|
174
|
-
|
|
184
|
+
|
|
185
|
+
// Recommended entrypoint (reads `Bun.argv.slice(2)`)
|
|
186
|
+
run(): Promise<void>;
|
|
187
|
+
|
|
188
|
+
// Useful for tests or programmatic invocation
|
|
189
|
+
runFromArgs(argv: string[]): Promise<void>;
|
|
190
|
+
|
|
175
191
|
getContext(): AppContext;
|
|
176
|
-
|
|
192
|
+
|
|
177
193
|
// Lifecycle hooks (override in subclass)
|
|
178
194
|
onBeforeRun?(command: Command, options: Record<string, unknown>): void;
|
|
179
195
|
onAfterRun?(command: Command, result: unknown): void;
|
|
@@ -196,6 +212,8 @@ Access application-wide services and configuration:
|
|
|
196
212
|
|
|
197
213
|
```typescript
|
|
198
214
|
import { AppContext } from "@pablozaiden/terminatui";
|
|
215
|
+
import type { CommandExecutionContext } from "@pablozaiden/terminatui";
|
|
216
|
+
import { AbortError } from "@pablozaiden/terminatui";
|
|
199
217
|
|
|
200
218
|
// Get the current context (set during Application.run())
|
|
201
219
|
const ctx = AppContext.current;
|
|
@@ -234,7 +252,7 @@ interface OptionDef {
|
|
|
234
252
|
tuiHidden?: boolean; // Hide from TUI form
|
|
235
253
|
}
|
|
236
254
|
|
|
237
|
-
type OptionSchema = Record<string, OptionDef>;
|
|
255
|
+
type OptionSchema = Record<string, OptionDef>; // See library types
|
|
238
256
|
```
|
|
239
257
|
|
|
240
258
|
## Config Validation with buildConfig
|
|
@@ -242,7 +260,7 @@ type OptionSchema = Record<string, OptionDef>;
|
|
|
242
260
|
Use `buildConfig()` to transform and validate options before execution:
|
|
243
261
|
|
|
244
262
|
```typescript
|
|
245
|
-
import { Command, ConfigValidationError, type
|
|
263
|
+
import { Command, ConfigValidationError, type OptionValues } from "@pablozaiden/terminatui";
|
|
246
264
|
|
|
247
265
|
interface MyConfig {
|
|
248
266
|
resolvedPath: string;
|
|
@@ -254,7 +272,7 @@ class MyCommand extends Command<typeof myOptions, MyConfig> {
|
|
|
254
272
|
readonly description = "Do something";
|
|
255
273
|
readonly options = myOptions;
|
|
256
274
|
|
|
257
|
-
override buildConfig(
|
|
275
|
+
override buildConfig(opts: OptionValues<typeof myOptions>): MyConfig {
|
|
258
276
|
const pathRaw = opts["path"] as string | undefined;
|
|
259
277
|
if (!pathRaw) {
|
|
260
278
|
throw new ConfigValidationError("Missing required option: path", "path");
|
|
@@ -271,9 +289,16 @@ class MyCommand extends Command<typeof myOptions, MyConfig> {
|
|
|
271
289
|
};
|
|
272
290
|
}
|
|
273
291
|
|
|
274
|
-
override async execute(
|
|
292
|
+
override async execute(
|
|
293
|
+
config: MyConfig,
|
|
294
|
+
execCtx: CommandExecutionContext
|
|
295
|
+
): Promise<CommandResult> {
|
|
275
296
|
// config is now typed as MyConfig
|
|
276
|
-
|
|
297
|
+
if (execCtx.signal.aborted) {
|
|
298
|
+
throw new AbortError("Command was cancelled");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
AppContext.current.logger.info(`Processing ${config.count} items from ${config.resolvedPath}`);
|
|
277
302
|
return { success: true };
|
|
278
303
|
}
|
|
279
304
|
}
|
|
@@ -288,19 +313,18 @@ class LongRunningCommand extends Command<typeof options> {
|
|
|
288
313
|
// ...
|
|
289
314
|
|
|
290
315
|
override async execute(
|
|
291
|
-
ctx: AppContext,
|
|
292
316
|
config: Config,
|
|
293
|
-
execCtx
|
|
317
|
+
execCtx: CommandExecutionContext
|
|
294
318
|
): Promise<CommandResult> {
|
|
295
319
|
for (const item of items) {
|
|
296
320
|
// Check for cancellation
|
|
297
|
-
if (execCtx
|
|
321
|
+
if (execCtx.signal.aborted) {
|
|
298
322
|
throw new AbortError("Command was cancelled");
|
|
299
323
|
}
|
|
300
|
-
|
|
301
|
-
await processItem(item, execCtx
|
|
324
|
+
|
|
325
|
+
await processItem(item, execCtx.signal);
|
|
302
326
|
}
|
|
303
|
-
|
|
327
|
+
|
|
304
328
|
return { success: true };
|
|
305
329
|
}
|
|
306
330
|
}
|
|
@@ -350,6 +374,9 @@ Extend `TuiApplication` instead of `Application` to get automatic TUI support:
|
|
|
350
374
|
import { TuiApplication, Command } from "@pablozaiden/terminatui";
|
|
351
375
|
|
|
352
376
|
class MyApp extends TuiApplication {
|
|
377
|
+
// Each app decides what "default" means.
|
|
378
|
+
protected override defaultRenderer = "opentui" as const;
|
|
379
|
+
|
|
353
380
|
constructor() {
|
|
354
381
|
super({
|
|
355
382
|
name: "myapp",
|
|
@@ -362,11 +389,13 @@ class MyApp extends TuiApplication {
|
|
|
362
389
|
}
|
|
363
390
|
```
|
|
364
391
|
|
|
365
|
-
|
|
392
|
+
Execution mode is controlled only by the selected mode (`--mode`) or the app’s configured default mode.
|
|
366
393
|
|
|
367
394
|
```bash
|
|
368
|
-
myapp
|
|
369
|
-
myapp
|
|
395
|
+
myapp # Uses app default mode
|
|
396
|
+
myapp --mode opentui # Forces TUI (OpenTUI)
|
|
397
|
+
myapp --mode ink # Forces TUI (Ink)
|
|
398
|
+
myapp --mode cli run --verbose # Forces CLI
|
|
370
399
|
```
|
|
371
400
|
|
|
372
401
|
### TUI Metadata
|
|
@@ -412,14 +441,15 @@ class RunCommand extends Command<typeof runOptions, RunConfig> {
|
|
|
412
441
|
override readonly immediateExecution = false; // Run immediately on selection
|
|
413
442
|
|
|
414
443
|
// Return structured results for display
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
444
|
+
override async execute(config: RunConfig, _execCtx: CommandExecutionContext): Promise<CommandResult> {
|
|
445
|
+
const result = await runTask(config);
|
|
446
|
+
return {
|
|
447
|
+
success: true,
|
|
448
|
+
data: result,
|
|
449
|
+
message: "Task completed",
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
423
453
|
|
|
424
454
|
// Custom result rendering (React/TSX)
|
|
425
455
|
override renderResult(result: CommandResult): ReactNode {
|
|
@@ -452,7 +482,7 @@ The built-in TUI provides:
|
|
|
452
482
|
- **Command Selector** - Navigate and select commands with arrow keys
|
|
453
483
|
- **Config Form** - Auto-generated forms from option schemas with field groups
|
|
454
484
|
- **Field Editor** - Edit field values (text, number, boolean, enum)
|
|
455
|
-
- **CLI
|
|
485
|
+
- **CLI Args Button** - View equivalent CLI command from the config form
|
|
456
486
|
- **Results Panel** - Display command results with custom rendering
|
|
457
487
|
- **Logs Panel** - View application logs in real-time
|
|
458
488
|
- **Clipboard Support** - Centralized copy with Ctrl+Y
|
|
@@ -464,9 +494,8 @@ The built-in TUI provides:
|
|
|
464
494
|
| Key | Action |
|
|
465
495
|
|-----|--------|
|
|
466
496
|
| ↑/↓ | Navigate fields/commands |
|
|
467
|
-
| Enter | Edit field / Execute command |
|
|
497
|
+
| Enter | Edit field / Execute command / Press button |
|
|
468
498
|
| Tab | Cycle focus between panels |
|
|
469
|
-
| C | Show CLI command modal |
|
|
470
499
|
| L | Toggle logs panel |
|
|
471
500
|
| Ctrl+Y | Copy current content to clipboard |
|
|
472
501
|
| Esc | Back / Cancel running command |
|
|
@@ -486,7 +515,7 @@ import {
|
|
|
486
515
|
useKeyboardHandler, // Register keyboard handlers with priority
|
|
487
516
|
useClipboard, // Clipboard operations with OSC 52
|
|
488
517
|
useLogStream, // Stream logs from logger
|
|
489
|
-
|
|
518
|
+
// (renderer-specific) Spinner UI lives in adapters
|
|
490
519
|
useCommandExecutor, // Execute commands with cancellation
|
|
491
520
|
|
|
492
521
|
// Context
|
|
@@ -494,7 +523,6 @@ import {
|
|
|
494
523
|
KeyboardPriority, // Global < Focused < Modal
|
|
495
524
|
|
|
496
525
|
// Components
|
|
497
|
-
Theme, // TUI color theme
|
|
498
526
|
JsonHighlight, // Syntax-highlighted JSON display
|
|
499
527
|
} from "@pablozaiden/terminatui";
|
|
500
528
|
```
|
|
@@ -504,19 +532,12 @@ import {
|
|
|
504
532
|
Terminatui includes utilities for formatted CLI output:
|
|
505
533
|
|
|
506
534
|
```typescript
|
|
507
|
-
import { colors
|
|
535
|
+
import { colors } from "@pablozaiden/terminatui";
|
|
508
536
|
|
|
509
537
|
// Colors
|
|
510
538
|
console.log(colors.red("Error!"));
|
|
511
539
|
console.log(colors.success("Done!")); // ✓ Done!
|
|
512
540
|
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
541
|
```
|
|
521
542
|
|
|
522
543
|
## License
|
package/package.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pablozaiden/terminatui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Terminal UI and Command Line Application Framework",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "https://github.com/PabloZaiden/terminatui"
|
|
7
7
|
},
|
|
8
8
|
"type": "module",
|
|
9
|
-
"
|
|
10
|
-
|
|
9
|
+
"files": [
|
|
10
|
+
"src/**/*"
|
|
11
|
+
],
|
|
11
12
|
"exports": {
|
|
12
|
-
".":
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"./hooks": "./src/hooks/index.ts"
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./src/index.ts",
|
|
15
|
+
"default": "./src/index.ts"
|
|
16
|
+
}
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "bunx tsc --noEmit",
|
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@opentui/react": "0.1.68",
|
|
25
|
+
"ink": "^6.6.0",
|
|
26
|
+
"ink-select-input": "^6.2.0",
|
|
27
|
+
"ink-text-input": "^6.0.0",
|
|
25
28
|
"tslog": "^4.9.3"
|
|
26
29
|
},
|
|
27
30
|
"devDependencies": {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { describe, test, expect
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
2
|
import { Application } from "../core/application.ts";
|
|
3
3
|
import { Command } from "../core/command.ts";
|
|
4
|
-
import { AppContext } from "../core/context.ts";
|
|
5
4
|
import type { OptionSchema, OptionValues, OptionDef } from "../types/command.ts";
|
|
5
|
+
import { AppContext } from "../core/context.ts";
|
|
6
|
+
import { LogLevel } from "../core/logger.ts";
|
|
7
|
+
import { KNOWN_COMMANDS } from "../core/knownCommands.ts";
|
|
6
8
|
|
|
7
9
|
// Define a proper option schema
|
|
8
10
|
const testOptions = {
|
|
@@ -21,7 +23,6 @@ class TestCommand extends Command<typeof testOptions> {
|
|
|
21
23
|
executedWith: Record<string, unknown> | null = null;
|
|
22
24
|
|
|
23
25
|
override async execute(
|
|
24
|
-
_ctx: AppContext,
|
|
25
26
|
opts: OptionValues<typeof testOptions>
|
|
26
27
|
): Promise<void> {
|
|
27
28
|
this.executedWith = opts as Record<string, unknown>;
|
|
@@ -35,17 +36,57 @@ class TuiCommand extends Command<OptionSchema> {
|
|
|
35
36
|
|
|
36
37
|
executed = false;
|
|
37
38
|
|
|
38
|
-
override async execute(
|
|
39
|
+
override async execute(): Promise<void> {
|
|
39
40
|
this.executed = true;
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
describe("Application", () => {
|
|
44
|
-
afterEach(() => {
|
|
45
|
-
AppContext.clearCurrent();
|
|
46
|
-
});
|
|
47
|
-
|
|
48
45
|
describe("constructor", () => {
|
|
46
|
+
test("rejects reserved help command definitions", () => {
|
|
47
|
+
class ReservedCommand extends Command<OptionSchema> {
|
|
48
|
+
readonly name = KNOWN_COMMANDS.help;
|
|
49
|
+
readonly description = "tries to override built-in";
|
|
50
|
+
readonly options = {};
|
|
51
|
+
|
|
52
|
+
override async execute(): Promise<void> {}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
expect(() => {
|
|
56
|
+
new Application({
|
|
57
|
+
name: "test-app",
|
|
58
|
+
version: "1.0.0",
|
|
59
|
+
commands: [new ReservedCommand()],
|
|
60
|
+
});
|
|
61
|
+
}).toThrow(/reserved/i);
|
|
62
|
+
|
|
63
|
+
class SubCommand extends Command<OptionSchema> {
|
|
64
|
+
readonly name = KNOWN_COMMANDS.help;
|
|
65
|
+
readonly description = "user help";
|
|
66
|
+
readonly options = {};
|
|
67
|
+
|
|
68
|
+
override async execute(): Promise<void> {}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
class ParentCommand extends Command<OptionSchema> {
|
|
72
|
+
readonly name = "parent";
|
|
73
|
+
readonly description = "parent";
|
|
74
|
+
readonly options = {};
|
|
75
|
+
|
|
76
|
+
override subCommands = [new SubCommand()];
|
|
77
|
+
|
|
78
|
+
override async execute(): Promise<void> {}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
expect(() => {
|
|
82
|
+
new Application({
|
|
83
|
+
name: "test-app",
|
|
84
|
+
version: "1.0.0",
|
|
85
|
+
commands: [new ParentCommand()],
|
|
86
|
+
});
|
|
87
|
+
}).toThrow(/automatically injected/i);
|
|
88
|
+
});
|
|
89
|
+
|
|
49
90
|
test("creates application with name and version", () => {
|
|
50
91
|
const app = new Application({
|
|
51
92
|
name: "test-app",
|
|
@@ -56,14 +97,16 @@ describe("Application", () => {
|
|
|
56
97
|
expect(app.version).toBe("1.0.0");
|
|
57
98
|
});
|
|
58
99
|
|
|
59
|
-
test("creates context
|
|
60
|
-
|
|
100
|
+
test("creates context as side effect of creating application", () => {
|
|
101
|
+
// side effect of creating an application is setting the current context
|
|
102
|
+
new Application({
|
|
61
103
|
name: "test-app",
|
|
62
104
|
version: "1.0.0",
|
|
63
105
|
commands: [],
|
|
64
106
|
});
|
|
65
|
-
|
|
66
|
-
expect(
|
|
107
|
+
|
|
108
|
+
expect(AppContext.current.config.name).toBe("test-app");
|
|
109
|
+
expect(AppContext.current.config.version).toBe("1.0.0");
|
|
67
110
|
});
|
|
68
111
|
|
|
69
112
|
test("registers provided commands", () => {
|
|
@@ -91,7 +134,7 @@ describe("Application", () => {
|
|
|
91
134
|
version: "1.0.0",
|
|
92
135
|
commands: [],
|
|
93
136
|
});
|
|
94
|
-
expect(app.registry.has(
|
|
137
|
+
expect(app.registry.has(KNOWN_COMMANDS.help)).toBe(true);
|
|
95
138
|
});
|
|
96
139
|
|
|
97
140
|
test("injects help subcommand into commands", () => {
|
|
@@ -103,32 +146,11 @@ describe("Application", () => {
|
|
|
103
146
|
commands: [cmd],
|
|
104
147
|
});
|
|
105
148
|
expect(cmd.subCommands).toBeDefined();
|
|
106
|
-
expect(cmd.subCommands?.some((c) => c.name ===
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
describe("getContext", () => {
|
|
111
|
-
test("returns the application context", () => {
|
|
112
|
-
const app = new Application({
|
|
113
|
-
name: "test-app",
|
|
114
|
-
version: "1.0.0",
|
|
115
|
-
commands: [],
|
|
116
|
-
});
|
|
117
|
-
expect(app.getContext()).toBe(app.context);
|
|
149
|
+
expect(cmd.subCommands?.some((c) => c.name === KNOWN_COMMANDS.help)).toBe(true);
|
|
118
150
|
});
|
|
119
151
|
});
|
|
120
152
|
|
|
121
153
|
describe("run", () => {
|
|
122
|
-
test("shows help when no args and no default command", async () => {
|
|
123
|
-
const app = new Application({
|
|
124
|
-
name: "test-app",
|
|
125
|
-
version: "1.0.0",
|
|
126
|
-
commands: [new TestCommand()],
|
|
127
|
-
});
|
|
128
|
-
// Should not throw
|
|
129
|
-
await app.run([]);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
154
|
test("runs default command when no args", async () => {
|
|
133
155
|
const cmd = new TuiCommand();
|
|
134
156
|
const app = new Application({
|
|
@@ -137,31 +159,30 @@ describe("Application", () => {
|
|
|
137
159
|
commands: [cmd],
|
|
138
160
|
defaultCommand: "tui-cmd",
|
|
139
161
|
});
|
|
140
|
-
await app.
|
|
162
|
+
await app.runFromArgs([]);
|
|
141
163
|
expect(cmd.executed).toBe(true);
|
|
142
164
|
});
|
|
143
165
|
|
|
144
|
-
test("runs specified command", async () => {
|
|
166
|
+
test("runs specified command and passes options", async () => {
|
|
145
167
|
const cmd = new TestCommand();
|
|
146
168
|
const app = new Application({
|
|
147
169
|
name: "test-app",
|
|
148
170
|
version: "1.0.0",
|
|
149
171
|
commands: [cmd],
|
|
150
172
|
});
|
|
151
|
-
|
|
152
|
-
|
|
173
|
+
|
|
174
|
+
await app.runFromArgs(["test", "--value", "hello"]);
|
|
175
|
+
expect(cmd.executedWith?.["value"]).toBe("hello");
|
|
153
176
|
});
|
|
154
177
|
|
|
155
|
-
test("
|
|
156
|
-
const cmd = new TestCommand();
|
|
178
|
+
test("with no args and no default, prints help (no throw)", async () => {
|
|
157
179
|
const app = new Application({
|
|
158
180
|
name: "test-app",
|
|
159
181
|
version: "1.0.0",
|
|
160
|
-
commands: [
|
|
182
|
+
commands: [new TestCommand()],
|
|
161
183
|
});
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
expect(cmd.executedWith?.["value"]).toBe("hello");
|
|
184
|
+
|
|
185
|
+
await app.runFromArgs([]);
|
|
165
186
|
});
|
|
166
187
|
});
|
|
167
188
|
|
|
@@ -179,7 +200,7 @@ describe("Application", () => {
|
|
|
179
200
|
called = true;
|
|
180
201
|
},
|
|
181
202
|
});
|
|
182
|
-
await app.
|
|
203
|
+
await app.runFromArgs(["test"]);
|
|
183
204
|
expect(called).toBe(true);
|
|
184
205
|
});
|
|
185
206
|
|
|
@@ -196,7 +217,7 @@ describe("Application", () => {
|
|
|
196
217
|
called = true;
|
|
197
218
|
},
|
|
198
219
|
});
|
|
199
|
-
await app.
|
|
220
|
+
await app.runFromArgs(["test"]);
|
|
200
221
|
expect(called).toBe(true);
|
|
201
222
|
});
|
|
202
223
|
|
|
@@ -219,11 +240,11 @@ describe("Application", () => {
|
|
|
219
240
|
commands: [new ErrorCommand()],
|
|
220
241
|
});
|
|
221
242
|
app.setHooks({
|
|
222
|
-
onError: async (
|
|
243
|
+
onError: async (error) => {
|
|
223
244
|
errorCaught = error;
|
|
224
245
|
},
|
|
225
246
|
});
|
|
226
|
-
await app.
|
|
247
|
+
await app.runFromArgs(["error-cmd"]);
|
|
227
248
|
expect(errorCaught?.message).toBe("Test error");
|
|
228
249
|
});
|
|
229
250
|
});
|
|
@@ -250,7 +271,6 @@ describe("Application", () => {
|
|
|
250
271
|
readonly options = configOptions;
|
|
251
272
|
|
|
252
273
|
override buildConfig(
|
|
253
|
-
_ctx: AppContext,
|
|
254
274
|
opts: OptionValues<typeof configOptions>
|
|
255
275
|
): ParsedConfig {
|
|
256
276
|
buildConfigCalled = true;
|
|
@@ -260,7 +280,7 @@ describe("Application", () => {
|
|
|
260
280
|
};
|
|
261
281
|
}
|
|
262
282
|
|
|
263
|
-
override async execute(
|
|
283
|
+
override async execute(config: ParsedConfig): Promise<void> {
|
|
264
284
|
receivedConfig = config;
|
|
265
285
|
}
|
|
266
286
|
}
|
|
@@ -271,7 +291,7 @@ describe("Application", () => {
|
|
|
271
291
|
commands: [new ConfigCommand()],
|
|
272
292
|
});
|
|
273
293
|
|
|
274
|
-
await app.
|
|
294
|
+
await app.runFromArgs(["config-cmd", "--value", "test", "--count", "42"]);
|
|
275
295
|
|
|
276
296
|
expect(buildConfigCalled).toBe(true);
|
|
277
297
|
expect(receivedConfig).toEqual({ value: "test", count: 42 });
|
|
@@ -286,7 +306,6 @@ describe("Application", () => {
|
|
|
286
306
|
readonly options = testOptions;
|
|
287
307
|
|
|
288
308
|
override async execute(
|
|
289
|
-
_ctx: AppContext,
|
|
290
309
|
opts: OptionValues<typeof testOptions>
|
|
291
310
|
): Promise<void> {
|
|
292
311
|
receivedOpts = opts as Record<string, unknown>;
|
|
@@ -299,7 +318,7 @@ describe("Application", () => {
|
|
|
299
318
|
commands: [new NoConfigCommand()],
|
|
300
319
|
});
|
|
301
320
|
|
|
302
|
-
await app.
|
|
321
|
+
await app.runFromArgs(["no-config-cmd", "--value", "hello"]);
|
|
303
322
|
|
|
304
323
|
expect(receivedOpts).toEqual({ value: "hello" });
|
|
305
324
|
});
|
|
@@ -328,12 +347,12 @@ describe("Application", () => {
|
|
|
328
347
|
});
|
|
329
348
|
|
|
330
349
|
app.setHooks({
|
|
331
|
-
onError: async (
|
|
350
|
+
onError: async (error) => {
|
|
332
351
|
errorCaught = error;
|
|
333
352
|
},
|
|
334
353
|
});
|
|
335
354
|
|
|
336
|
-
await app.
|
|
355
|
+
await app.runFromArgs(["fail-config", "--value", "test"]);
|
|
337
356
|
|
|
338
357
|
expect(errorCaught?.message).toBe("Config validation failed");
|
|
339
358
|
});
|
|
@@ -349,7 +368,7 @@ describe("Application", () => {
|
|
|
349
368
|
});
|
|
350
369
|
|
|
351
370
|
// Should not throw - global option should be parsed and removed
|
|
352
|
-
await app.
|
|
371
|
+
await app.runFromArgs(["--log-level", "debug", "test", "--value", "hello"]);
|
|
353
372
|
expect(cmd.executedWith?.["value"]).toBe("hello");
|
|
354
373
|
});
|
|
355
374
|
|
|
@@ -361,7 +380,7 @@ describe("Application", () => {
|
|
|
361
380
|
commands: [cmd],
|
|
362
381
|
});
|
|
363
382
|
|
|
364
|
-
await app.
|
|
383
|
+
await app.runFromArgs(["test", "--log-level", "debug", "--value", "hello"]);
|
|
365
384
|
expect(cmd.executedWith?.["value"]).toBe("hello");
|
|
366
385
|
});
|
|
367
386
|
|
|
@@ -374,14 +393,14 @@ describe("Application", () => {
|
|
|
374
393
|
});
|
|
375
394
|
|
|
376
395
|
// All of these should work (case-insensitive)
|
|
377
|
-
await app.
|
|
378
|
-
expect(
|
|
396
|
+
await app.runFromArgs(["--log-level", "debug", "test"]);
|
|
397
|
+
expect(AppContext.current.logger.getMinLevel()).toBe(LogLevel.debug);
|
|
379
398
|
|
|
380
|
-
await app.
|
|
381
|
-
expect(
|
|
399
|
+
await app.runFromArgs(["--log-level", "Debug", "test"]);
|
|
400
|
+
expect(AppContext.current.logger.getMinLevel()).toBe(LogLevel.debug);
|
|
382
401
|
|
|
383
|
-
await app.
|
|
384
|
-
expect(
|
|
402
|
+
await app.runFromArgs(["--log-level", "DEBUG", "test"]);
|
|
403
|
+
expect(AppContext.current.logger.getMinLevel()).toBe(LogLevel.debug);
|
|
385
404
|
});
|
|
386
405
|
|
|
387
406
|
test("parses --detailed-logs flag", async () => {
|
|
@@ -392,7 +411,7 @@ describe("Application", () => {
|
|
|
392
411
|
commands: [cmd],
|
|
393
412
|
});
|
|
394
413
|
|
|
395
|
-
await app.
|
|
414
|
+
await app.runFromArgs(["--detailed-logs", "test"]);
|
|
396
415
|
// Should not throw - flag is recognized
|
|
397
416
|
expect(cmd.executedWith).not.toBeNull();
|
|
398
417
|
});
|
|
@@ -405,7 +424,7 @@ describe("Application", () => {
|
|
|
405
424
|
commands: [cmd],
|
|
406
425
|
});
|
|
407
426
|
|
|
408
|
-
await app.
|
|
427
|
+
await app.runFromArgs(["--no-detailed-logs", "test"]);
|
|
409
428
|
// Should not throw - flag is recognized
|
|
410
429
|
expect(cmd.executedWith).not.toBeNull();
|
|
411
430
|
});
|
|
@@ -418,8 +437,8 @@ describe("Application", () => {
|
|
|
418
437
|
commands: [cmd],
|
|
419
438
|
});
|
|
420
439
|
|
|
421
|
-
await app.
|
|
422
|
-
expect(
|
|
440
|
+
await app.runFromArgs(["--log-level=warn", "test"]);
|
|
441
|
+
expect(AppContext.current.logger.getMinLevel()).toBe(LogLevel.warn);
|
|
423
442
|
});
|
|
424
443
|
});
|
|
425
444
|
});
|