@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.
Files changed (181) hide show
  1. package/README.md +64 -43
  2. package/package.json +11 -8
  3. package/src/__tests__/application.test.ts +87 -68
  4. package/src/__tests__/buildCliCommand.test.ts +99 -119
  5. package/src/__tests__/builtins.test.ts +27 -75
  6. package/src/__tests__/command.test.ts +100 -131
  7. package/src/__tests__/configOnChange.test.ts +63 -0
  8. package/src/__tests__/context.test.ts +1 -26
  9. package/src/__tests__/helpCore.test.ts +227 -0
  10. package/src/__tests__/parser.test.ts +98 -244
  11. package/src/__tests__/registry.test.ts +33 -160
  12. package/src/__tests__/schemaToFields.test.ts +75 -158
  13. package/src/builtins/help.ts +12 -4
  14. package/src/builtins/settings.ts +18 -32
  15. package/src/builtins/version.ts +3 -3
  16. package/src/cli/output/colors.ts +1 -1
  17. package/src/cli/parser.ts +26 -95
  18. package/src/core/application.ts +192 -110
  19. package/src/core/command.ts +26 -9
  20. package/src/core/context.ts +31 -20
  21. package/src/core/help.ts +24 -18
  22. package/src/core/knownCommands.ts +13 -0
  23. package/src/core/logger.ts +39 -42
  24. package/src/core/registry.ts +5 -12
  25. package/src/index.ts +22 -137
  26. package/src/tui/TuiApplication.tsx +63 -120
  27. package/src/tui/TuiRoot.tsx +135 -0
  28. package/src/tui/adapters/factory.ts +19 -0
  29. package/src/tui/adapters/ink/InkRenderer.tsx +139 -0
  30. package/src/tui/adapters/ink/components/Button.tsx +12 -0
  31. package/src/tui/adapters/ink/components/Code.tsx +6 -0
  32. package/src/tui/adapters/ink/components/CodeHighlight.tsx +6 -0
  33. package/src/tui/adapters/ink/components/Container.tsx +5 -0
  34. package/src/tui/adapters/ink/components/Field.tsx +12 -0
  35. package/src/tui/adapters/ink/components/Label.tsx +24 -0
  36. package/src/tui/adapters/ink/components/MenuButton.tsx +12 -0
  37. package/src/tui/adapters/ink/components/MenuItem.tsx +17 -0
  38. package/src/tui/adapters/ink/components/Overlay.tsx +5 -0
  39. package/src/tui/adapters/ink/components/Panel.tsx +15 -0
  40. package/src/tui/adapters/ink/components/ScrollView.tsx +5 -0
  41. package/src/tui/adapters/ink/components/Select.tsx +44 -0
  42. package/src/tui/adapters/ink/components/Spacer.tsx +15 -0
  43. package/src/tui/adapters/ink/components/Spinner.tsx +5 -0
  44. package/src/tui/adapters/ink/components/TextInput.tsx +22 -0
  45. package/src/tui/adapters/ink/components/Value.tsx +7 -0
  46. package/src/tui/adapters/ink/keyboard.ts +97 -0
  47. package/src/tui/adapters/ink/utils.ts +16 -0
  48. package/src/tui/adapters/opentui/OpenTuiRenderer.tsx +119 -0
  49. package/src/tui/adapters/opentui/components/Button.tsx +13 -0
  50. package/src/tui/adapters/opentui/components/Code.tsx +12 -0
  51. package/src/tui/adapters/opentui/components/CodeHighlight.tsx +24 -0
  52. package/src/tui/adapters/opentui/components/Container.tsx +56 -0
  53. package/src/tui/adapters/opentui/components/Field.tsx +18 -0
  54. package/src/tui/adapters/opentui/components/Label.tsx +15 -0
  55. package/src/tui/adapters/opentui/components/MenuButton.tsx +14 -0
  56. package/src/tui/adapters/opentui/components/MenuItem.tsx +29 -0
  57. package/src/tui/adapters/opentui/components/Overlay.tsx +21 -0
  58. package/src/tui/adapters/opentui/components/Panel.tsx +78 -0
  59. package/src/tui/adapters/opentui/components/ScrollView.tsx +85 -0
  60. package/src/tui/adapters/opentui/components/Select.tsx +59 -0
  61. package/src/tui/adapters/opentui/components/Spacer.tsx +5 -0
  62. package/src/tui/adapters/opentui/components/Spinner.tsx +12 -0
  63. package/src/tui/adapters/opentui/components/TextInput.tsx +13 -0
  64. package/src/tui/adapters/opentui/components/Value.tsx +13 -0
  65. package/src/tui/{hooks → adapters/opentui/hooks}/useSpinner.ts +2 -11
  66. package/src/tui/adapters/opentui/keyboard.ts +61 -0
  67. package/src/tui/adapters/types.ts +71 -0
  68. package/src/tui/components/ActionButton.tsx +0 -36
  69. package/src/tui/components/CommandSelector.tsx +45 -92
  70. package/src/tui/components/ConfigForm.tsx +68 -42
  71. package/src/tui/components/FieldRow.tsx +0 -30
  72. package/src/tui/components/Header.tsx +14 -13
  73. package/src/tui/components/JsonHighlight.tsx +10 -17
  74. package/src/tui/components/ModalBase.tsx +38 -0
  75. package/src/tui/components/ResultsPanel.tsx +27 -36
  76. package/src/tui/components/StatusBar.tsx +24 -39
  77. package/src/tui/components/logColors.ts +12 -0
  78. package/src/tui/context/ClipboardContext.tsx +87 -0
  79. package/src/tui/context/ExecutorContext.tsx +139 -0
  80. package/src/tui/context/KeyboardContext.tsx +85 -71
  81. package/src/tui/context/LogsContext.tsx +35 -0
  82. package/src/tui/context/NavigationContext.tsx +194 -0
  83. package/src/tui/context/RendererContext.tsx +20 -0
  84. package/src/tui/context/TuiAppContext.tsx +58 -0
  85. package/src/tui/hooks/useActiveKeyHandler.ts +75 -0
  86. package/src/tui/hooks/useBackHandler.ts +34 -0
  87. package/src/tui/hooks/useClipboard.ts +40 -25
  88. package/src/tui/hooks/useClipboardProvider.ts +42 -0
  89. package/src/tui/hooks/useGlobalKeyHandler.ts +54 -0
  90. package/src/tui/modals/CliModal.tsx +82 -0
  91. package/src/tui/modals/EditorModal.tsx +207 -0
  92. package/src/tui/modals/LogsModal.tsx +98 -0
  93. package/src/tui/registry.ts +102 -0
  94. package/src/tui/screens/CommandSelectScreen.tsx +162 -0
  95. package/src/tui/screens/ConfigScreen.tsx +165 -0
  96. package/src/tui/screens/ErrorScreen.tsx +58 -0
  97. package/src/tui/screens/ResultsScreen.tsx +68 -0
  98. package/src/tui/screens/RunningScreen.tsx +72 -0
  99. package/src/tui/screens/ScreenBase.ts +6 -0
  100. package/src/tui/semantic/Button.tsx +7 -0
  101. package/src/tui/semantic/Code.tsx +7 -0
  102. package/src/tui/semantic/CodeHighlight.tsx +7 -0
  103. package/src/tui/semantic/Container.tsx +7 -0
  104. package/src/tui/semantic/Field.tsx +7 -0
  105. package/src/tui/semantic/Label.tsx +7 -0
  106. package/src/tui/semantic/MenuButton.tsx +7 -0
  107. package/src/tui/semantic/MenuItem.tsx +7 -0
  108. package/src/tui/semantic/Overlay.tsx +7 -0
  109. package/src/tui/semantic/Panel.tsx +7 -0
  110. package/src/tui/semantic/ScrollView.tsx +9 -0
  111. package/src/tui/semantic/Select.tsx +7 -0
  112. package/src/tui/semantic/Spacer.tsx +7 -0
  113. package/src/tui/semantic/Spinner.tsx +7 -0
  114. package/src/tui/semantic/TextInput.tsx +7 -0
  115. package/src/tui/semantic/Value.tsx +7 -0
  116. package/src/tui/semantic/types.ts +195 -0
  117. package/src/tui/theme.ts +25 -14
  118. package/src/tui/utils/buildCliCommand.ts +1 -0
  119. package/src/tui/utils/getEnumKeys.ts +3 -0
  120. package/src/tui/utils/parameterPersistence.ts +1 -0
  121. package/src/types/command.ts +0 -60
  122. package/.devcontainer/devcontainer.json +0 -19
  123. package/.devcontainer/install-prerequisites.sh +0 -49
  124. package/.github/workflows/copilot-setup-steps.yml +0 -32
  125. package/.github/workflows/pull-request.yml +0 -27
  126. package/.github/workflows/release-npm-package.yml +0 -81
  127. package/AGENTS.md +0 -31
  128. package/bun.lock +0 -236
  129. package/examples/tui-app/commands/config/app/get.ts +0 -66
  130. package/examples/tui-app/commands/config/app/index.ts +0 -27
  131. package/examples/tui-app/commands/config/app/set.ts +0 -86
  132. package/examples/tui-app/commands/config/index.ts +0 -32
  133. package/examples/tui-app/commands/config/user/get.ts +0 -65
  134. package/examples/tui-app/commands/config/user/index.ts +0 -27
  135. package/examples/tui-app/commands/config/user/set.ts +0 -61
  136. package/examples/tui-app/commands/greet.ts +0 -76
  137. package/examples/tui-app/commands/index.ts +0 -4
  138. package/examples/tui-app/commands/math.ts +0 -115
  139. package/examples/tui-app/commands/status.ts +0 -77
  140. package/examples/tui-app/index.ts +0 -35
  141. package/guides/01-hello-world.md +0 -96
  142. package/guides/02-adding-options.md +0 -103
  143. package/guides/03-multiple-commands.md +0 -163
  144. package/guides/04-subcommands.md +0 -206
  145. package/guides/05-interactive-tui.md +0 -194
  146. package/guides/06-config-validation.md +0 -264
  147. package/guides/07-async-cancellation.md +0 -336
  148. package/guides/08-complete-application.md +0 -537
  149. package/guides/README.md +0 -74
  150. package/src/__tests__/colors.test.ts +0 -127
  151. package/src/__tests__/commandClass.test.ts +0 -130
  152. package/src/__tests__/help.test.ts +0 -412
  153. package/src/__tests__/registryNew.test.ts +0 -160
  154. package/src/__tests__/table.test.ts +0 -146
  155. package/src/__tests__/tui.test.ts +0 -26
  156. package/src/builtins/index.ts +0 -4
  157. package/src/cli/help.ts +0 -174
  158. package/src/cli/index.ts +0 -3
  159. package/src/cli/output/index.ts +0 -2
  160. package/src/cli/output/table.ts +0 -141
  161. package/src/commands/help.ts +0 -50
  162. package/src/commands/index.ts +0 -1
  163. package/src/components/index.ts +0 -147
  164. package/src/core/index.ts +0 -15
  165. package/src/hooks/index.ts +0 -131
  166. package/src/registry/commandRegistry.ts +0 -77
  167. package/src/registry/index.ts +0 -1
  168. package/src/tui/TuiApp.tsx +0 -619
  169. package/src/tui/app.ts +0 -29
  170. package/src/tui/components/CliModal.tsx +0 -81
  171. package/src/tui/components/EditorModal.tsx +0 -177
  172. package/src/tui/components/LogsPanel.tsx +0 -86
  173. package/src/tui/components/index.ts +0 -13
  174. package/src/tui/context/index.ts +0 -7
  175. package/src/tui/hooks/index.ts +0 -35
  176. package/src/tui/hooks/useKeyboardHandler.ts +0 -91
  177. package/src/tui/hooks/useLogStream.ts +0 -96
  178. package/src/tui/index.ts +0 -65
  179. package/src/tui/utils/index.ts +0 -13
  180. package/src/types/index.ts +0 -1
  181. package/tsconfig.json +0 -25
@@ -1,206 +0,0 @@
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)
@@ -1,194 +0,0 @@
1
- # Guide 5: Interactive TUI (Normal)
2
-
3
- Add an auto-generated Terminal User Interface to your CLI.
4
-
5
- ## What You'll Build
6
-
7
- A task runner with both CLI and interactive TUI modes:
8
-
9
- ```bash
10
- # CLI mode
11
- taskr run --task build --env production
12
-
13
- # TUI mode (interactive)
14
- taskr
15
- ```
16
-
17
- When you run without arguments, an interactive form appears!
18
-
19
- ## Step 1: Create the Command with TUI Metadata
20
-
21
- Create `src/commands/run.ts`:
22
-
23
- ```typescript
24
- import { Command, type AppContext, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
25
-
26
- const options = {
27
- task: {
28
- type: "string",
29
- description: "Task to run",
30
- required: true,
31
- enum: ["build", "test", "lint", "deploy"],
32
- // TUI metadata
33
- label: "Task",
34
- order: 1,
35
- group: "Required",
36
- },
37
- env: {
38
- type: "string",
39
- description: "Environment",
40
- default: "development",
41
- enum: ["development", "staging", "production"],
42
- // TUI metadata
43
- label: "Environment",
44
- order: 2,
45
- group: "Configuration",
46
- },
47
- verbose: {
48
- type: "boolean",
49
- description: "Verbose output",
50
- default: false,
51
- // TUI metadata
52
- label: "Verbose Mode",
53
- order: 10,
54
- group: "Options",
55
- },
56
- } satisfies OptionSchema;
57
-
58
- interface RunConfig {
59
- task: string;
60
- env: string;
61
- verbose: boolean;
62
- }
63
-
64
- export class RunCommand extends Command<typeof options, RunConfig> {
65
- readonly name = "run";
66
- readonly description = "Run a task";
67
- readonly options = options;
68
-
69
- // TUI customization
70
- override readonly displayName = "Run Task";
71
- override readonly actionLabel = "Start Task";
72
-
73
- async execute(ctx: AppContext, config: RunConfig): Promise<CommandResult> {
74
- ctx.logger.info(`Starting task: ${config.task}`);
75
-
76
- if (config.verbose) {
77
- ctx.logger.debug(`Environment: ${config.env}`);
78
- }
79
-
80
- // Simulate task execution
81
- console.log(`Running ${config.task} in ${config.env}...`);
82
- await new Promise((resolve) => setTimeout(resolve, 1000));
83
- console.log("Task completed!");
84
-
85
- return {
86
- success: true,
87
- data: { task: config.task, env: config.env },
88
- message: `Task ${config.task} completed successfully`
89
- };
90
- }
91
- }
92
- ```
93
-
94
- ## Step 2: Create the TUI Application
95
-
96
- Create `src/index.ts`:
97
-
98
- ```typescript
99
- import { TuiApplication } from "@pablozaiden/terminatui";
100
- import { RunCommand } from "./commands/run";
101
-
102
- class TaskRunnerApp extends TuiApplication {
103
- constructor() {
104
- super({
105
- name: "taskr",
106
- displayName: "🚀 Task Runner", // Shown in TUI header
107
- version: "1.0.0",
108
- commands: [new RunCommand()],
109
- enableTui: true, // Default: true
110
- });
111
- }
112
- }
113
-
114
- await new TaskRunnerApp().run();
115
- ```
116
-
117
- ## Step 3: Test Both Modes
118
-
119
- **CLI Mode** (with arguments):
120
-
121
- ```bash
122
- bun src/index.ts run --task build --env production
123
- # Running build in production...
124
- # Task completed!
125
- ```
126
-
127
- **TUI Mode** (no arguments):
128
-
129
- ```bash
130
- bun src/index.ts
131
- ```
132
-
133
- This opens an interactive interface:
134
- - Use ↑/↓ to navigate fields
135
- - Press Enter to edit a field
136
- - Press Enter on "Start Task" to run
137
- - Press Esc to go back
138
- - Press C to see the CLI command
139
-
140
- ## TUI Metadata Reference
141
-
142
- Add these properties to your options for TUI customization:
143
-
144
- ```typescript
145
- {
146
- type: "string",
147
- description: "...",
148
-
149
- // TUI-specific
150
- label: "Display Label", // Custom field label
151
- order: 1, // Field sort order
152
- group: "Settings", // Group heading
153
- placeholder: "Enter...", // Placeholder text
154
- tuiHidden: false, // Hide from TUI (still in CLI)
155
- }
156
- ```
157
-
158
- ## Command TUI Properties
159
-
160
- ```typescript
161
- class MyCommand extends Command {
162
- // Display name in command selector
163
- override readonly displayName = "My Command";
164
-
165
- // Button text (default: "Run")
166
- override readonly actionLabel = "Execute";
167
-
168
- // Skip config screen, run immediately
169
- override readonly immediateExecution = false;
170
- }
171
- ```
172
-
173
- ## Keyboard Shortcuts
174
-
175
- | Key | Action |
176
- |-----|--------|
177
- | ↑/↓ | Navigate fields |
178
- | Enter | Edit field / Run |
179
- | Tab | Cycle focus |
180
- | C | Show CLI command |
181
- | L | Toggle logs |
182
- | Ctrl+Y | Copy to clipboard |
183
- | Esc | Back / Cancel |
184
-
185
- ## What You Learned
186
-
187
- - Use `TuiApplication` instead of `Application`
188
- - Add TUI metadata to options (label, order, group)
189
- - Customize with `displayName` and `actionLabel`
190
- - Both CLI and TUI work with the same command
191
-
192
- ## Next Steps
193
-
194
- → [Guide 6: Config Validation](06-config-validation.md)
@@ -1,264 +0,0 @@
1
- # Guide 6: Config Validation (Normal)
2
-
3
- Transform and validate options before execution with `buildConfig`.
4
-
5
- ## What You'll Build
6
-
7
- A deploy CLI that validates paths, resolves environment configs, and provides helpful error messages:
8
-
9
- ```bash
10
- deploy --app ./myapp --env production --replicas 3
11
- ```
12
-
13
- ## Step 1: Define Options and Config Types
14
-
15
- Create `src/commands/deploy.ts`:
16
-
17
- ```typescript
18
- import path from "node:path";
19
- import { existsSync } from "node:fs";
20
- import {
21
- Command,
22
- ConfigValidationError,
23
- type AppContext,
24
- type OptionSchema,
25
- type OptionValues,
26
- type CommandResult
27
- } from "@pablozaiden/terminatui";
28
-
29
- // Raw CLI options
30
- const options = {
31
- app: {
32
- type: "string",
33
- description: "Path to application",
34
- required: true,
35
- },
36
- env: {
37
- type: "string",
38
- description: "Deployment environment",
39
- required: true,
40
- enum: ["development", "staging", "production"],
41
- },
42
- replicas: {
43
- type: "string", // CLI args are strings
44
- description: "Number of replicas",
45
- default: "1",
46
- },
47
- "dry-run": {
48
- type: "boolean",
49
- description: "Preview without deploying",
50
- default: false,
51
- },
52
- } satisfies OptionSchema;
53
-
54
- // Validated config type
55
- interface DeployConfig {
56
- appPath: string; // Resolved absolute path
57
- appName: string; // Extracted from path
58
- environment: string;
59
- replicas: number; // Parsed to number
60
- dryRun: boolean;
61
- envConfig: { // Environment-specific settings
62
- url: string;
63
- timeout: number;
64
- };
65
- }
66
- ```
67
-
68
- ## Step 2: Implement buildConfig
69
-
70
- ```typescript
71
- // Environment-specific configurations
72
- const ENV_CONFIGS = {
73
- development: { url: "http://localhost:3000", timeout: 5000 },
74
- staging: { url: "https://staging.example.com", timeout: 10000 },
75
- production: { url: "https://example.com", timeout: 30000 },
76
- };
77
-
78
- export class DeployCommand extends Command<typeof options, DeployConfig> {
79
- readonly name = "deploy";
80
- readonly description = "Deploy an application";
81
- readonly options = options;
82
-
83
- /**
84
- * Transform and validate raw options into DeployConfig.
85
- * Runs before execute() - errors here show helpful messages.
86
- */
87
- override buildConfig(
88
- _ctx: AppContext,
89
- opts: OptionValues<typeof options>
90
- ): DeployConfig {
91
- // 1. Validate app path exists
92
- const appRaw = opts["app"] as string | undefined;
93
- if (!appRaw) {
94
- throw new ConfigValidationError(
95
- "Missing required option: app",
96
- "app" // Field to highlight in TUI
97
- );
98
- }
99
-
100
- const appPath = path.resolve(appRaw);
101
- if (!existsSync(appPath)) {
102
- throw new ConfigValidationError(
103
- `Application path does not exist: ${appPath}`,
104
- "app"
105
- );
106
- }
107
-
108
- // 2. Extract app name from path
109
- const appName = path.basename(appPath);
110
-
111
- // 3. Validate environment
112
- const environment = opts["env"] as string;
113
- if (!environment) {
114
- throw new ConfigValidationError(
115
- "Missing required option: env",
116
- "env"
117
- );
118
- }
119
-
120
- // 4. Parse and validate replicas
121
- const replicasStr = opts["replicas"] as string ?? "1";
122
- const replicas = parseInt(replicasStr, 10);
123
-
124
- if (isNaN(replicas)) {
125
- throw new ConfigValidationError(
126
- `Replicas must be a number, got: ${replicasStr}`,
127
- "replicas"
128
- );
129
- }
130
-
131
- if (replicas < 1 || replicas > 10) {
132
- throw new ConfigValidationError(
133
- "Replicas must be between 1 and 10",
134
- "replicas"
135
- );
136
- }
137
-
138
- // 5. Get environment-specific config
139
- const envConfig = ENV_CONFIGS[environment as keyof typeof ENV_CONFIGS];
140
-
141
- // 6. Return validated config
142
- return {
143
- appPath,
144
- appName,
145
- environment,
146
- replicas,
147
- dryRun: opts["dry-run"] as boolean ?? false,
148
- envConfig,
149
- };
150
- }
151
- ```
152
-
153
- ## Step 3: Implement execute with clean config
154
-
155
- ```typescript
156
- /**
157
- * Execute with fully validated DeployConfig.
158
- * No need to validate here - buildConfig already did it!
159
- */
160
- async execute(ctx: AppContext, config: DeployConfig): Promise<CommandResult> {
161
- ctx.logger.info(`Deploying ${config.appName} to ${config.environment}`);
162
- ctx.logger.debug(`Path: ${config.appPath}`);
163
- ctx.logger.debug(`Replicas: ${config.replicas}`);
164
- ctx.logger.debug(`URL: ${config.envConfig.url}`);
165
-
166
- if (config.dryRun) {
167
- console.log("DRY RUN - Would deploy:");
168
- console.log(` App: ${config.appName}`);
169
- console.log(` Environment: ${config.environment}`);
170
- console.log(` Replicas: ${config.replicas}`);
171
- console.log(` Target: ${config.envConfig.url}`);
172
- return { success: true, message: "Dry run completed" };
173
- }
174
-
175
- console.log(`Deploying ${config.appName}...`);
176
- console.log(` Creating ${config.replicas} replicas...`);
177
- console.log(` Targeting ${config.envConfig.url}...`);
178
-
179
- // Simulate deployment
180
- await new Promise((resolve) => setTimeout(resolve, 2000));
181
-
182
- console.log("Deployment successful!");
183
-
184
- return {
185
- success: true,
186
- data: {
187
- app: config.appName,
188
- environment: config.environment,
189
- replicas: config.replicas,
190
- url: config.envConfig.url,
191
- },
192
- message: `Deployed ${config.appName} to ${config.environment}`
193
- };
194
- }
195
- }
196
- ```
197
-
198
- ## Step 4: Create the Application
199
-
200
- Create `src/index.ts`:
201
-
202
- ```typescript
203
- import { TuiApplication } from "@pablozaiden/terminatui";
204
- import { DeployCommand } from "./commands/deploy";
205
-
206
- class DeployCLI extends TuiApplication {
207
- constructor() {
208
- super({
209
- name: "deploy",
210
- version: "1.0.0",
211
- commands: [new DeployCommand()],
212
- });
213
- }
214
- }
215
-
216
- await new DeployCLI().run();
217
- ```
218
-
219
- ## Step 5: Test Validation
220
-
221
- ```bash
222
- # Missing required option
223
- bun src/index.ts deploy --env production
224
- # Error: Missing required option: app
225
-
226
- # Invalid path
227
- bun src/index.ts deploy --app ./nonexistent --env staging
228
- # Error: Application path does not exist: /path/to/nonexistent
229
-
230
- # Invalid replicas
231
- bun src/index.ts deploy --app . --env production --replicas abc
232
- # Error: Replicas must be a number, got: abc
233
-
234
- # Out of range replicas
235
- bun src/index.ts deploy --app . --env production --replicas 100
236
- # Error: Replicas must be between 1 and 10
237
-
238
- # Dry run (valid)
239
- bun src/index.ts deploy --app . --env production --replicas 3 --dry-run
240
- # DRY RUN - Would deploy: ...
241
-
242
- # Full deploy
243
- bun src/index.ts deploy --app . --env staging --replicas 2
244
- # Deploying myapp...
245
- ```
246
-
247
- ## Benefits of buildConfig
248
-
249
- 1. **Separation of concerns** - Validation separate from logic
250
- 2. **Type safety** - `execute()` receives validated `DeployConfig`
251
- 3. **Better errors** - `ConfigValidationError` highlights fields in TUI
252
- 4. **Reusable** - Works for both CLI and TUI modes
253
- 5. **Testable** - Easy to unit test validation logic
254
-
255
- ## What You Learned
256
-
257
- - Use `buildConfig` to transform and validate options
258
- - Throw `ConfigValidationError` with field name for TUI highlighting
259
- - Parse strings to numbers and resolve paths
260
- - Keep `execute()` clean with pre-validated config
261
-
262
- ## Next Steps
263
-
264
- → [Guide 7: Async Commands with Cancellation](07-async-cancellation.md)