@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,194 @@
|
|
|
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)
|
|
@@ -0,0 +1,264 @@
|
|
|
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)
|