@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,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 statusOptions = {
|
|
10
|
+
detailed: {
|
|
11
|
+
type: "boolean",
|
|
12
|
+
description: "Show detailed status",
|
|
13
|
+
default: false,
|
|
14
|
+
label: "Detailed",
|
|
15
|
+
order: 1,
|
|
16
|
+
},
|
|
17
|
+
} as const satisfies OptionSchema;
|
|
18
|
+
|
|
19
|
+
export class StatusCommand extends Command<typeof statusOptions> {
|
|
20
|
+
readonly name = "status";
|
|
21
|
+
readonly description = "Show application status";
|
|
22
|
+
readonly options = statusOptions;
|
|
23
|
+
|
|
24
|
+
override readonly actionLabel = "Check Status";
|
|
25
|
+
override readonly immediateExecution = true; // No required fields
|
|
26
|
+
|
|
27
|
+
override async execute(ctx: AppContext, opts: OptionValues<typeof statusOptions>): Promise<CommandResult> {
|
|
28
|
+
const result = await this.getStatus(opts);
|
|
29
|
+
ctx.logger.info(result.message || "Status check complete");
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
override renderResult(result: CommandResult): string {
|
|
34
|
+
if (!result.success) return "❌ Status check failed";
|
|
35
|
+
const data = result.data as { uptime: string; memory: string; platform: string; version: string };
|
|
36
|
+
return [
|
|
37
|
+
"✓ Application Status",
|
|
38
|
+
"",
|
|
39
|
+
` Uptime: ${data.uptime}`,
|
|
40
|
+
` Memory: ${data.memory}`,
|
|
41
|
+
` Platform: ${data.platform}`,
|
|
42
|
+
` Node: ${data.version}`,
|
|
43
|
+
].join("\n");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private async getStatus(opts: OptionValues<typeof statusOptions>): Promise<CommandResult> {
|
|
47
|
+
const detailed = opts.detailed as boolean;
|
|
48
|
+
|
|
49
|
+
// Simulate some async work
|
|
50
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
51
|
+
|
|
52
|
+
const memMB = Math.round(process.memoryUsage().heapUsed / 1024 / 1024);
|
|
53
|
+
const uptimeSec = Math.round(process.uptime());
|
|
54
|
+
|
|
55
|
+
const data: Record<string, unknown> = {
|
|
56
|
+
uptime: `${uptimeSec} seconds`,
|
|
57
|
+
memory: `${memMB} MB`,
|
|
58
|
+
platform: process.platform,
|
|
59
|
+
version: Bun.version,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (detailed) {
|
|
63
|
+
Object.assign(data, {
|
|
64
|
+
cwd: process.cwd(),
|
|
65
|
+
pid: process.pid,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
success: true,
|
|
71
|
+
data,
|
|
72
|
+
message: "All systems operational",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Example TUI Application
|
|
4
|
+
*
|
|
5
|
+
* Demonstrates how to use TuiApplication to build a CLI/TUI app
|
|
6
|
+
* with minimal effort. Just run:
|
|
7
|
+
*
|
|
8
|
+
* bun examples/tui-app/index.ts
|
|
9
|
+
*
|
|
10
|
+
* Or in CLI mode:
|
|
11
|
+
*
|
|
12
|
+
* bun examples/tui-app/index.ts greet --name "World" --loud
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { TuiApplication } from "../../src/index.ts";
|
|
16
|
+
import { GreetCommand, MathCommand, StatusCommand } from "./commands/index.ts";
|
|
17
|
+
|
|
18
|
+
class ExampleApp extends TuiApplication {
|
|
19
|
+
constructor() {
|
|
20
|
+
super({
|
|
21
|
+
name: "example",
|
|
22
|
+
version: "1.0.0",
|
|
23
|
+
commands: [
|
|
24
|
+
new GreetCommand(),
|
|
25
|
+
new MathCommand(),
|
|
26
|
+
new StatusCommand(),
|
|
27
|
+
],
|
|
28
|
+
enableTui: true,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Run the app
|
|
34
|
+
await new ExampleApp().run(Bun.argv.slice(2));
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Guide 1: Hello World CLI (Super Simple)
|
|
2
|
+
|
|
3
|
+
Create your first CLI app with Terminatui in under 5 minutes.
|
|
4
|
+
|
|
5
|
+
## What You'll Build
|
|
6
|
+
|
|
7
|
+
A simple CLI that greets the user by name.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
myapp greet --name Alice
|
|
11
|
+
# Output: Hello, Alice!
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Prerequisites
|
|
15
|
+
|
|
16
|
+
- Bun installed (`curl -fsSL https://bun.sh/install | bash`)
|
|
17
|
+
|
|
18
|
+
## Step 1: Create Project
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
mkdir hello-cli && cd hello-cli
|
|
22
|
+
bun init -y
|
|
23
|
+
bun add @pablozaiden/terminatui
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Step 2: Create the Command
|
|
27
|
+
|
|
28
|
+
Create `src/greet.ts`:
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { Command, type AppContext, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
|
|
32
|
+
|
|
33
|
+
const options = {
|
|
34
|
+
name: {
|
|
35
|
+
type: "string",
|
|
36
|
+
description: "Name to greet",
|
|
37
|
+
required: true,
|
|
38
|
+
},
|
|
39
|
+
} satisfies OptionSchema;
|
|
40
|
+
|
|
41
|
+
export class GreetCommand extends Command<typeof options> {
|
|
42
|
+
readonly name = "greet";
|
|
43
|
+
readonly description = "Greet someone";
|
|
44
|
+
readonly options = options;
|
|
45
|
+
|
|
46
|
+
execute(_ctx: AppContext, config: { name: string }): CommandResult {
|
|
47
|
+
console.log(`Hello, ${config.name}!`);
|
|
48
|
+
return { success: true };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Step 3: Create the App
|
|
54
|
+
|
|
55
|
+
Create `src/index.ts`:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { Application } from "@pablozaiden/terminatui";
|
|
59
|
+
import { GreetCommand } from "./greet";
|
|
60
|
+
|
|
61
|
+
class MyApp extends Application {
|
|
62
|
+
constructor() {
|
|
63
|
+
super({
|
|
64
|
+
name: "myapp",
|
|
65
|
+
version: "1.0.0",
|
|
66
|
+
commands: [new GreetCommand()],
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
await new MyApp().run();
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Step 4: Run It
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
bun src/index.ts greet --name Alice
|
|
78
|
+
# Output: Hello, Alice!
|
|
79
|
+
|
|
80
|
+
bun src/index.ts help
|
|
81
|
+
# Shows available commands
|
|
82
|
+
|
|
83
|
+
bun src/index.ts greet help
|
|
84
|
+
# Shows greet options
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## What You Learned
|
|
88
|
+
|
|
89
|
+
- Commands extend the `Command` class
|
|
90
|
+
- Options are defined with `OptionSchema`
|
|
91
|
+
- The `execute()` method handles the logic
|
|
92
|
+
- `Application` ties everything together
|
|
93
|
+
|
|
94
|
+
## Next Steps
|
|
95
|
+
|
|
96
|
+
→ [Guide 2: Adding Options](02-adding-options.md)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Guide 2: Adding Options (Super Simple)
|
|
2
|
+
|
|
3
|
+
Add boolean and string options to make your CLI more flexible.
|
|
4
|
+
|
|
5
|
+
## What You'll Build
|
|
6
|
+
|
|
7
|
+
Extend the greeting command with a `--loud` flag:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
myapp greet --name Alice
|
|
11
|
+
# Output: Hello, Alice!
|
|
12
|
+
|
|
13
|
+
myapp greet --name Alice --loud
|
|
14
|
+
# Output: HELLO, ALICE!
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Step 1: Add the Option
|
|
18
|
+
|
|
19
|
+
Update `src/greet.ts`:
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { Command, type AppContext, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
|
|
23
|
+
|
|
24
|
+
const options = {
|
|
25
|
+
name: {
|
|
26
|
+
type: "string",
|
|
27
|
+
description: "Name to greet",
|
|
28
|
+
required: true,
|
|
29
|
+
},
|
|
30
|
+
loud: {
|
|
31
|
+
type: "boolean",
|
|
32
|
+
description: "Shout the greeting",
|
|
33
|
+
alias: "l", // Short flag: -l
|
|
34
|
+
default: false,
|
|
35
|
+
},
|
|
36
|
+
} satisfies OptionSchema;
|
|
37
|
+
|
|
38
|
+
export class GreetCommand extends Command<typeof options> {
|
|
39
|
+
readonly name = "greet";
|
|
40
|
+
readonly description = "Greet someone";
|
|
41
|
+
readonly options = options;
|
|
42
|
+
|
|
43
|
+
execute(_ctx: AppContext, config: { name: string; loud: boolean }): CommandResult {
|
|
44
|
+
const message = `Hello, ${config.name}!`;
|
|
45
|
+
console.log(config.loud ? message.toUpperCase() : message);
|
|
46
|
+
return { success: true };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Step 2: Test It
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Normal greeting
|
|
55
|
+
bun src/index.ts greet --name Alice
|
|
56
|
+
# Hello, Alice!
|
|
57
|
+
|
|
58
|
+
# Loud greeting with long flag
|
|
59
|
+
bun src/index.ts greet --name Bob --loud
|
|
60
|
+
# HELLO, BOB!
|
|
61
|
+
|
|
62
|
+
# Loud greeting with short flag
|
|
63
|
+
bun src/index.ts greet --name Charlie -l
|
|
64
|
+
# HELLO, CHARLIE!
|
|
65
|
+
|
|
66
|
+
# View help to see options
|
|
67
|
+
bun src/index.ts greet help
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Key Concepts
|
|
71
|
+
|
|
72
|
+
### Option Types
|
|
73
|
+
|
|
74
|
+
| Type | Description | Example |
|
|
75
|
+
|------|-------------|---------|
|
|
76
|
+
| `string` | Text value | `--name Alice` |
|
|
77
|
+
| `boolean` | Flag (true/false) | `--loud` or `--no-loud` |
|
|
78
|
+
| `number` | Numeric value | `--count 5` |
|
|
79
|
+
| `array` | Multiple values | `--tags a --tags b` |
|
|
80
|
+
|
|
81
|
+
### Option Properties
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
{
|
|
85
|
+
type: "string",
|
|
86
|
+
description: "Help text", // Required
|
|
87
|
+
required: true, // Must be provided
|
|
88
|
+
default: "value", // Default if not provided
|
|
89
|
+
alias: "n", // Short flag (-n)
|
|
90
|
+
enum: ["a", "b", "c"], // Restrict to values
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## What You Learned
|
|
95
|
+
|
|
96
|
+
- Add options with different types
|
|
97
|
+
- Use `alias` for short flags (-l instead of --loud)
|
|
98
|
+
- Use `default` for optional values
|
|
99
|
+
- Boolean flags can be negated with `--no-` prefix
|
|
100
|
+
|
|
101
|
+
## Next Steps
|
|
102
|
+
|
|
103
|
+
→ [Guide 3: Multiple Commands](03-multiple-commands.md)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Guide 3: Multiple Commands (Basic)
|
|
2
|
+
|
|
3
|
+
Build a CLI with multiple commands that share common functionality.
|
|
4
|
+
|
|
5
|
+
## What You'll Build
|
|
6
|
+
|
|
7
|
+
A file utility CLI with `list` and `count` commands:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
fileutil list --dir ./src
|
|
11
|
+
# Lists files in directory
|
|
12
|
+
|
|
13
|
+
fileutil count --dir ./src --ext .ts
|
|
14
|
+
# Counts files with extension
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Step 1: Create the List Command
|
|
18
|
+
|
|
19
|
+
Create `src/commands/list.ts`:
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { readdirSync } from "fs";
|
|
23
|
+
import { Command, type AppContext, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
|
|
24
|
+
|
|
25
|
+
const options = {
|
|
26
|
+
dir: {
|
|
27
|
+
type: "string",
|
|
28
|
+
description: "Directory to list",
|
|
29
|
+
required: true,
|
|
30
|
+
alias: "d",
|
|
31
|
+
},
|
|
32
|
+
} satisfies OptionSchema;
|
|
33
|
+
|
|
34
|
+
export class ListCommand extends Command<typeof options> {
|
|
35
|
+
readonly name = "list";
|
|
36
|
+
readonly description = "List files in a directory";
|
|
37
|
+
readonly options = options;
|
|
38
|
+
|
|
39
|
+
execute(_ctx: AppContext, config: { dir: string }): CommandResult {
|
|
40
|
+
try {
|
|
41
|
+
const files = readdirSync(config.dir);
|
|
42
|
+
console.log(`Files in ${config.dir}:`);
|
|
43
|
+
files.forEach((file) => console.log(` ${file}`));
|
|
44
|
+
return { success: true, message: `Found ${files.length} files` };
|
|
45
|
+
} catch (error) {
|
|
46
|
+
return { success: false, error: String(error) };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Step 2: Create the Count Command
|
|
53
|
+
|
|
54
|
+
Create `src/commands/count.ts`:
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { readdirSync } from "fs";
|
|
58
|
+
import { Command, type AppContext, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
|
|
59
|
+
|
|
60
|
+
const options = {
|
|
61
|
+
dir: {
|
|
62
|
+
type: "string",
|
|
63
|
+
description: "Directory to search",
|
|
64
|
+
required: true,
|
|
65
|
+
alias: "d",
|
|
66
|
+
},
|
|
67
|
+
ext: {
|
|
68
|
+
type: "string",
|
|
69
|
+
description: "File extension to count (e.g., .ts)",
|
|
70
|
+
alias: "e",
|
|
71
|
+
},
|
|
72
|
+
} satisfies OptionSchema;
|
|
73
|
+
|
|
74
|
+
export class CountCommand extends Command<typeof options> {
|
|
75
|
+
readonly name = "count";
|
|
76
|
+
readonly description = "Count files in a directory";
|
|
77
|
+
readonly options = options;
|
|
78
|
+
|
|
79
|
+
execute(_ctx: AppContext, config: { dir: string; ext?: string }): CommandResult {
|
|
80
|
+
try {
|
|
81
|
+
let files = readdirSync(config.dir);
|
|
82
|
+
|
|
83
|
+
if (config.ext) {
|
|
84
|
+
files = files.filter((f) => f.endsWith(config.ext!));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log(`Found ${files.length} files${config.ext ? ` with ${config.ext}` : ""}`);
|
|
88
|
+
return { success: true, data: { count: files.length } };
|
|
89
|
+
} catch (error) {
|
|
90
|
+
return { success: false, error: String(error) };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Step 3: Create the Application
|
|
97
|
+
|
|
98
|
+
Create `src/index.ts`:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { Application } from "@pablozaiden/terminatui";
|
|
102
|
+
import { ListCommand } from "./commands/list";
|
|
103
|
+
import { CountCommand } from "./commands/count";
|
|
104
|
+
|
|
105
|
+
class FileUtilApp extends Application {
|
|
106
|
+
constructor() {
|
|
107
|
+
super({
|
|
108
|
+
name: "fileutil",
|
|
109
|
+
version: "1.0.0",
|
|
110
|
+
description: "File utility commands",
|
|
111
|
+
commands: [
|
|
112
|
+
new ListCommand(),
|
|
113
|
+
new CountCommand(),
|
|
114
|
+
],
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await new FileUtilApp().run();
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Step 4: Test It
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# List files
|
|
126
|
+
bun src/index.ts list --dir ./src
|
|
127
|
+
# Files in ./src:
|
|
128
|
+
# commands
|
|
129
|
+
# index.ts
|
|
130
|
+
|
|
131
|
+
# Count all files
|
|
132
|
+
bun src/index.ts count -d ./src
|
|
133
|
+
# Found 2 files
|
|
134
|
+
|
|
135
|
+
# Count only .ts files
|
|
136
|
+
bun src/index.ts count -d ./src -e .ts
|
|
137
|
+
# Found 1 files with .ts
|
|
138
|
+
|
|
139
|
+
# Show help
|
|
140
|
+
bun src/index.ts help
|
|
141
|
+
# Lists: list, count, version, help
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Project Structure
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
src/
|
|
148
|
+
├── index.ts # App entry point
|
|
149
|
+
└── commands/
|
|
150
|
+
├── list.ts # List command
|
|
151
|
+
└── count.ts # Count command
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## What You Learned
|
|
155
|
+
|
|
156
|
+
- Create multiple commands in separate files
|
|
157
|
+
- Return structured `CommandResult` with data
|
|
158
|
+
- Handle errors gracefully
|
|
159
|
+
- Use consistent option patterns across commands
|
|
160
|
+
|
|
161
|
+
## Next Steps
|
|
162
|
+
|
|
163
|
+
→ [Guide 4: Subcommands](04-subcommands.md)
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Guide 4: Subcommands (Basic)
|
|
2
|
+
|
|
3
|
+
Organize related commands under a parent command for cleaner CLI structure.
|
|
4
|
+
|
|
5
|
+
## What You'll Build
|
|
6
|
+
|
|
7
|
+
A database CLI with nested subcommands:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
dbctl db migrate --target latest
|
|
11
|
+
dbctl db seed --file data.json
|
|
12
|
+
dbctl db status
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Step 1: Create the Subcommands
|
|
16
|
+
|
|
17
|
+
Create `src/commands/db/migrate.ts`:
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { Command, type AppContext, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
|
|
21
|
+
|
|
22
|
+
const options = {
|
|
23
|
+
target: {
|
|
24
|
+
type: "string",
|
|
25
|
+
description: "Migration target version",
|
|
26
|
+
default: "latest",
|
|
27
|
+
},
|
|
28
|
+
dry: {
|
|
29
|
+
type: "boolean",
|
|
30
|
+
description: "Dry run without applying",
|
|
31
|
+
default: false,
|
|
32
|
+
},
|
|
33
|
+
} satisfies OptionSchema;
|
|
34
|
+
|
|
35
|
+
export class MigrateCommand extends Command<typeof options> {
|
|
36
|
+
readonly name = "migrate";
|
|
37
|
+
readonly description = "Run database migrations";
|
|
38
|
+
readonly options = options;
|
|
39
|
+
|
|
40
|
+
execute(ctx: AppContext, config: { target: string; dry: boolean }): CommandResult {
|
|
41
|
+
ctx.logger.info(`Migrating to: ${config.target}`);
|
|
42
|
+
|
|
43
|
+
if (config.dry) {
|
|
44
|
+
console.log("DRY RUN: Would migrate to", config.target);
|
|
45
|
+
} else {
|
|
46
|
+
console.log("Migrating to", config.target, "...");
|
|
47
|
+
console.log("Migration complete!");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { success: true };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Create `src/commands/db/seed.ts`:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { Command, type AppContext, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
|
|
59
|
+
|
|
60
|
+
const options = {
|
|
61
|
+
file: {
|
|
62
|
+
type: "string",
|
|
63
|
+
description: "Seed data file",
|
|
64
|
+
required: true,
|
|
65
|
+
alias: "f",
|
|
66
|
+
},
|
|
67
|
+
} satisfies OptionSchema;
|
|
68
|
+
|
|
69
|
+
export class SeedCommand extends Command<typeof options> {
|
|
70
|
+
readonly name = "seed";
|
|
71
|
+
readonly description = "Seed the database with data";
|
|
72
|
+
readonly options = options;
|
|
73
|
+
|
|
74
|
+
execute(ctx: AppContext, config: { file: string }): CommandResult {
|
|
75
|
+
ctx.logger.info(`Seeding from: ${config.file}`);
|
|
76
|
+
console.log(`Loading seed data from ${config.file}...`);
|
|
77
|
+
console.log("Database seeded successfully!");
|
|
78
|
+
return { success: true };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Create `src/commands/db/status.ts`:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import { Command, type AppContext, type CommandResult } from "@pablozaiden/terminatui";
|
|
87
|
+
|
|
88
|
+
export class StatusCommand extends Command {
|
|
89
|
+
readonly name = "status";
|
|
90
|
+
readonly description = "Show database status";
|
|
91
|
+
readonly options = {};
|
|
92
|
+
|
|
93
|
+
execute(_ctx: AppContext): CommandResult {
|
|
94
|
+
console.log("Database Status:");
|
|
95
|
+
console.log(" Connected: Yes");
|
|
96
|
+
console.log(" Version: 1.2.3");
|
|
97
|
+
console.log(" Migrations: 5 applied");
|
|
98
|
+
return { success: true };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Step 2: Create the Parent Command
|
|
104
|
+
|
|
105
|
+
Create `src/commands/db/index.ts`:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import { Command, type AppContext, type CommandResult } from "@pablozaiden/terminatui";
|
|
109
|
+
import { MigrateCommand } from "./migrate";
|
|
110
|
+
import { SeedCommand } from "./seed";
|
|
111
|
+
import { StatusCommand } from "./status";
|
|
112
|
+
|
|
113
|
+
export class DbCommand extends Command {
|
|
114
|
+
readonly name = "db";
|
|
115
|
+
readonly description = "Database operations";
|
|
116
|
+
readonly options = {};
|
|
117
|
+
|
|
118
|
+
// Subcommands are nested here
|
|
119
|
+
override readonly subCommands = [
|
|
120
|
+
new MigrateCommand(),
|
|
121
|
+
new SeedCommand(),
|
|
122
|
+
new StatusCommand(),
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
// Parent command can have its own execute (optional)
|
|
126
|
+
execute(_ctx: AppContext): CommandResult {
|
|
127
|
+
console.log("Use 'dbctl db <command>' for database operations.");
|
|
128
|
+
console.log("Available: migrate, seed, status");
|
|
129
|
+
return { success: true };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Step 3: Create the Application
|
|
135
|
+
|
|
136
|
+
Create `src/index.ts`:
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
import { Application } from "@pablozaiden/terminatui";
|
|
140
|
+
import { DbCommand } from "./commands/db";
|
|
141
|
+
|
|
142
|
+
class DbCtlApp extends Application {
|
|
143
|
+
constructor() {
|
|
144
|
+
super({
|
|
145
|
+
name: "dbctl",
|
|
146
|
+
version: "1.0.0",
|
|
147
|
+
description: "Database control CLI",
|
|
148
|
+
commands: [new DbCommand()],
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
await new DbCtlApp().run();
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Step 4: Test It
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
# Show db command help
|
|
160
|
+
bun src/index.ts db help
|
|
161
|
+
# Shows: migrate, seed, status
|
|
162
|
+
|
|
163
|
+
# Run migration
|
|
164
|
+
bun src/index.ts db migrate
|
|
165
|
+
# Migrating to latest...
|
|
166
|
+
|
|
167
|
+
# Dry run migration
|
|
168
|
+
bun src/index.ts db migrate --target v2 --dry
|
|
169
|
+
# DRY RUN: Would migrate to v2
|
|
170
|
+
|
|
171
|
+
# Seed database
|
|
172
|
+
bun src/index.ts db seed -f data.json
|
|
173
|
+
# Loading seed data from data.json...
|
|
174
|
+
|
|
175
|
+
# Check status
|
|
176
|
+
bun src/index.ts db status
|
|
177
|
+
# Database Status: ...
|
|
178
|
+
|
|
179
|
+
# Get help for subcommand
|
|
180
|
+
bun src/index.ts db migrate help
|
|
181
|
+
# Shows migrate options
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Project Structure
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
src/
|
|
188
|
+
├── index.ts
|
|
189
|
+
└── commands/
|
|
190
|
+
└── db/
|
|
191
|
+
├── index.ts # Parent command
|
|
192
|
+
├── migrate.ts # Subcommand
|
|
193
|
+
├── seed.ts # Subcommand
|
|
194
|
+
└── status.ts # Subcommand
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## What You Learned
|
|
198
|
+
|
|
199
|
+
- Group related commands under a parent
|
|
200
|
+
- Define subcommands with `subCommands` property
|
|
201
|
+
- Each subcommand has its own options
|
|
202
|
+
- Help is automatically nested (`db help`, `db migrate help`)
|
|
203
|
+
|
|
204
|
+
## Next Steps
|
|
205
|
+
|
|
206
|
+
→ [Guide 5: Interactive TUI](05-interactive-tui.md)
|