@pablozaiden/terminatui 0.3.0-beta-1 → 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 (39) hide show
  1. package/package.json +10 -3
  2. package/src/__tests__/configOnChange.test.ts +63 -0
  3. package/src/builtins/version.ts +1 -1
  4. package/src/index.ts +22 -0
  5. package/src/tui/adapters/ink/InkRenderer.tsx +4 -0
  6. package/src/tui/adapters/opentui/OpenTuiRenderer.tsx +4 -0
  7. package/src/tui/adapters/types.ts +1 -0
  8. package/src/tui/screens/ConfigScreen.tsx +6 -1
  9. package/src/tui/screens/ResultsScreen.tsx +9 -1
  10. package/src/tui/screens/RunningScreen.tsx +1 -1
  11. package/.devcontainer/devcontainer.json +0 -19
  12. package/.devcontainer/install-prerequisites.sh +0 -49
  13. package/.github/workflows/copilot-setup-steps.yml +0 -32
  14. package/.github/workflows/pull-request.yml +0 -27
  15. package/.github/workflows/release-npm-package.yml +0 -81
  16. package/AGENTS.md +0 -43
  17. package/CLAUDE.md +0 -1
  18. package/bun.lock +0 -321
  19. package/examples/tui-app/commands/config/app/get.ts +0 -62
  20. package/examples/tui-app/commands/config/app/index.ts +0 -23
  21. package/examples/tui-app/commands/config/app/set.ts +0 -96
  22. package/examples/tui-app/commands/config/index.ts +0 -28
  23. package/examples/tui-app/commands/config/user/get.ts +0 -61
  24. package/examples/tui-app/commands/config/user/index.ts +0 -23
  25. package/examples/tui-app/commands/config/user/set.ts +0 -57
  26. package/examples/tui-app/commands/greet.ts +0 -78
  27. package/examples/tui-app/commands/math.ts +0 -111
  28. package/examples/tui-app/commands/status.ts +0 -86
  29. package/examples/tui-app/index.ts +0 -38
  30. package/guides/01-hello-world.md +0 -101
  31. package/guides/02-adding-options.md +0 -103
  32. package/guides/03-multiple-commands.md +0 -161
  33. package/guides/04-subcommands.md +0 -206
  34. package/guides/05-interactive-tui.md +0 -209
  35. package/guides/06-config-validation.md +0 -256
  36. package/guides/07-async-cancellation.md +0 -334
  37. package/guides/08-complete-application.md +0 -507
  38. package/guides/README.md +0 -78
  39. package/tsconfig.json +0 -25
@@ -1,256 +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 {
20
- Command,
21
- ConfigValidationError,
22
- type OptionSchema,
23
- type OptionValues,
24
- type CommandResult
25
- } from "@pablozaiden/terminatui";
26
-
27
- // Raw CLI options
28
- const options = {
29
- app: {
30
- type: "string",
31
- description: "Path to application",
32
- required: true,
33
- },
34
- env: {
35
- type: "string",
36
- description: "Deployment environment",
37
- required: true,
38
- enum: ["development", "staging", "production"],
39
- },
40
- replicas: {
41
- type: "string", // CLI args are strings
42
- description: "Number of replicas",
43
- default: "1",
44
- },
45
- "dry-run": {
46
- type: "boolean",
47
- description: "Preview without deploying",
48
- default: false,
49
- },
50
- } satisfies OptionSchema;
51
-
52
- // Validated config type
53
- interface DeployConfig {
54
- appPath: string; // Resolved absolute path
55
- appName: string; // Extracted from path
56
- environment: string;
57
- replicas: number; // Parsed to number
58
- dryRun: boolean;
59
- envConfig: { // Environment-specific settings
60
- url: string;
61
- timeout: number;
62
- };
63
- }
64
- ```
65
-
66
- ## Step 2: Implement buildConfig
67
-
68
- ```typescript
69
- // Environment-specific configurations
70
- const ENV_CONFIGS = {
71
- development: { url: "http://localhost:3000", timeout: 5000 },
72
- staging: { url: "https://staging.example.com", timeout: 10000 },
73
- production: { url: "https://example.com", timeout: 30000 },
74
- };
75
-
76
- export class DeployCommand extends Command<typeof options, DeployConfig> {
77
- readonly name = "deploy";
78
- readonly description = "Deploy an application";
79
- readonly options = options;
80
-
81
- /**
82
- * Transform and validate raw options into DeployConfig.
83
- * Runs before execute() - errors here show helpful messages.
84
- */
85
- override async buildConfig(opts: OptionValues<typeof options>): Promise<DeployConfig> {
86
- // 1. Validate app path exists
87
- const appRaw = opts["app"] as string | undefined;
88
- if (!appRaw) {
89
- throw new ConfigValidationError(
90
- "Missing required option: app",
91
- "app" // Field to highlight in TUI
92
- );
93
- }
94
-
95
- const appPath = path.resolve(appRaw);
96
- if (!(await Bun.file(appPath).exists())) {
97
- throw new ConfigValidationError(
98
- `Application path does not exist: ${appPath}`,
99
- "app"
100
- );
101
- }
102
-
103
- // 2. Extract app name from path
104
- const appName = path.basename(appPath);
105
-
106
- // 3. Validate environment
107
- const environment = opts["env"] as string;
108
- if (!environment) {
109
- throw new ConfigValidationError(
110
- "Missing required option: env",
111
- "env"
112
- );
113
- }
114
-
115
- // 4. Parse and validate replicas
116
- const replicasStr = opts["replicas"] as string ?? "1";
117
- const replicas = parseInt(replicasStr, 10);
118
-
119
- if (isNaN(replicas)) {
120
- throw new ConfigValidationError(
121
- `Replicas must be a number, got: ${replicasStr}`,
122
- "replicas"
123
- );
124
- }
125
-
126
- if (replicas < 1 || replicas > 10) {
127
- throw new ConfigValidationError(
128
- "Replicas must be between 1 and 10",
129
- "replicas"
130
- );
131
- }
132
-
133
- // 5. Get environment-specific config
134
- const envConfig = ENV_CONFIGS[environment as keyof typeof ENV_CONFIGS];
135
-
136
- // 6. Return validated config
137
- return {
138
- appPath,
139
- appName,
140
- environment,
141
- replicas,
142
- dryRun: opts["dry-run"] as boolean ?? false,
143
- envConfig,
144
- };
145
- }
146
- ```
147
-
148
- ## Step 3: Implement execute with clean config
149
-
150
- ```typescript
151
- /**
152
- * Execute with fully validated DeployConfig.
153
- * No need to validate here - buildConfig already did it!
154
- */
155
- async execute(config: DeployConfig): Promise<CommandResult> {
156
- console.log(`Deploying ${config.appName} to ${config.environment}`);
157
-
158
- if (config.dryRun) {
159
- console.log("DRY RUN - Would deploy:");
160
- console.log(` App: ${config.appName}`);
161
- console.log(` Environment: ${config.environment}`);
162
- console.log(` Replicas: ${config.replicas}`);
163
- console.log(` Target: ${config.envConfig.url}`);
164
- return { success: true, message: "Dry run completed" };
165
- }
166
-
167
- console.log(`Deploying ${config.appName}...`);
168
- console.log(` Creating ${config.replicas} replicas...`);
169
- console.log(` Targeting ${config.envConfig.url}...`);
170
-
171
- // Simulate deployment
172
- await new Promise((resolve) => setTimeout(resolve, 2000));
173
-
174
- console.log("Deployment successful!");
175
-
176
- return {
177
- success: true,
178
- data: {
179
- app: config.appName,
180
- environment: config.environment,
181
- replicas: config.replicas,
182
- url: config.envConfig.url,
183
- },
184
- message: `Deployed ${config.appName} to ${config.environment}`
185
- };
186
- }
187
- }
188
- ```
189
-
190
- ## Step 4: Create the Application
191
-
192
- Create `src/index.ts`:
193
-
194
- ```typescript
195
- import { TuiApplication } from "@pablozaiden/terminatui";
196
- import { DeployCommand } from "./commands/deploy";
197
-
198
- class DeployCLI extends TuiApplication {
199
- constructor() {
200
- super({
201
- name: "deploy",
202
- version: "1.0.0",
203
- commands: [new DeployCommand()],
204
- });
205
- }
206
- }
207
-
208
- await new DeployCLI().run();
209
- ```
210
-
211
- ## Step 5: Test Validation
212
-
213
- ```bash
214
- # Missing required option
215
- bun src/index.ts deploy --env production
216
- # Error: Missing required option: app
217
-
218
- # Invalid path
219
- bun src/index.ts deploy --app ./nonexistent --env staging
220
- # Error: Application path does not exist: /path/to/nonexistent
221
-
222
- # Invalid replicas
223
- bun src/index.ts deploy --app . --env production --replicas abc
224
- # Error: Replicas must be a number, got: abc
225
-
226
- # Out of range replicas
227
- bun src/index.ts deploy --app . --env production --replicas 100
228
- # Error: Replicas must be between 1 and 10
229
-
230
- # Dry run (valid)
231
- bun src/index.ts deploy --app . --env production --replicas 3 --dry-run
232
- # DRY RUN - Would deploy: ...
233
-
234
- # Full deploy
235
- bun src/index.ts deploy --app . --env staging --replicas 2
236
- # Deploying myapp...
237
- ```
238
-
239
- ## Benefits of buildConfig
240
-
241
- 1. **Separation of concerns** - Validation separate from logic
242
- 2. **Type safety** - `execute()` receives validated `DeployConfig`
243
- 3. **Better errors** - `ConfigValidationError` highlights fields in TUI
244
- 4. **Reusable** - Works for both CLI and TUI modes
245
- 5. **Testable** - Easy to unit test validation logic
246
-
247
- ## What You Learned
248
-
249
- - Use `buildConfig` to transform and validate options
250
- - Throw `ConfigValidationError` with field name for TUI highlighting
251
- - Parse strings to numbers and resolve paths
252
- - Keep `execute()` clean with pre-validated config
253
-
254
- ## Next Steps
255
-
256
- → [Guide 7: Async Commands with Cancellation](07-async-cancellation.md)
@@ -1,334 +0,0 @@
1
- # Guide 7: Async Commands with Cancellation (Complex)
2
-
3
- Build commands that support cancellation with proper cleanup for long-running operations.
4
-
5
- ## What You'll Build
6
-
7
- A download manager that:
8
- - Downloads files asynchronously
9
- - Shows progress updates
10
- - Supports cancellation (Ctrl+C in CLI, Esc in TUI)
11
- - Cleans up partial downloads when cancelled
12
-
13
- ```bash
14
- download https://example.com/large-file.zip --output ./downloads/
15
- ```
16
-
17
- ## Step 1: Define the Command
18
-
19
- Create `src/commands/download.ts`:
20
-
21
- ```typescript
22
- import path from "node:path";
23
- import {
24
- Command,
25
- ConfigValidationError,
26
- type OptionSchema,
27
- type OptionValues,
28
- type CommandResult,
29
- type CommandExecutionContext
30
- } from "@pablozaiden/terminatui";
31
-
32
- const options = {
33
- url: {
34
- type: "string",
35
- description: "URL to download",
36
- required: true,
37
- label: "Download URL",
38
- },
39
- output: {
40
- type: "string",
41
- description: "Output directory",
42
- default: "./downloads",
43
- label: "Output Directory",
44
- },
45
- "chunk-size": {
46
- type: "string",
47
- description: "Download chunk size in KB",
48
- default: "1024",
49
- label: "Chunk Size (KB)",
50
- },
51
- } satisfies OptionSchema;
52
-
53
- interface DownloadConfig {
54
- url: URL;
55
- outputDir: string;
56
- fileName: string;
57
- chunkSize: number;
58
- }
59
- ```
60
-
61
- ## Step 2: Implement buildConfig
62
-
63
- ```typescript
64
- export class DownloadCommand extends Command<typeof options, DownloadConfig> {
65
- readonly name = "download";
66
- readonly description = "Download a file from URL";
67
- readonly options = options;
68
- readonly displayName = "File Downloader";
69
- readonly actionLabel = "Download";
70
-
71
- override buildConfig(opts: OptionValues<typeof options>): DownloadConfig {
72
- // Validate URL
73
- const urlStr = opts["url"] as string;
74
- if (!urlStr) {
75
- throw new ConfigValidationError("URL is required", "url");
76
- }
77
-
78
- let url: URL;
79
- try {
80
- url = new URL(urlStr);
81
- } catch {
82
- throw new ConfigValidationError("Invalid URL format", "url");
83
- }
84
-
85
- // Validate output directory
86
- const outputDir = path.resolve(opts["output"] as string ?? "./downloads");
87
-
88
- // Extract filename from URL
89
- const fileName = path.basename(url.pathname) || "download";
90
-
91
- // Parse chunk size
92
- const chunkSizeStr = opts["chunk-size"] as string ?? "1024";
93
- const chunkSize = parseInt(chunkSizeStr, 10) * 1024; // Convert KB to bytes
94
- if (isNaN(chunkSize) || chunkSize <= 0) {
95
- throw new ConfigValidationError("Chunk size must be a positive number", "chunk-size");
96
- }
97
-
98
- return { url, outputDir, fileName, chunkSize };
99
- }
100
- ```
101
-
102
- ## Step 3: Implement Cancellable Download
103
-
104
- ```typescript
105
- async execute(
106
- config: DownloadConfig,
107
- execCtx: CommandExecutionContext
108
- ): Promise<CommandResult> {
109
- const { url, outputDir, fileName, chunkSize } = config;
110
- const outputPath = path.join(outputDir, fileName);
111
- const signal = execCtx.signal;
112
-
113
- console.log(`Starting download: ${url}`);
114
- console.log(`Output: ${outputPath}`);
115
-
116
- // Create output directory
117
- await Bun.write(path.join(outputDir, ".keep"), "");
118
-
119
- // Track download state for cleanup
120
- let downloadedBytes = 0;
121
- let totalBytes = 0;
122
- let partialFile: Bun.FileSink | null = null;
123
-
124
- try {
125
- // Check for cancellation before starting
126
- if (signal.aborted) {
127
-
128
- return { success: false, message: "Download cancelled before start" };
129
- }
130
-
131
- // Fetch with AbortSignal
132
- const response = await fetch(url.toString(), { signal });
133
-
134
- if (!response.ok) {
135
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
136
- }
137
-
138
- totalBytes = parseInt(response.headers.get("content-length") ?? "0", 10);
139
- const reader = response.body?.getReader();
140
-
141
- if (!reader) {
142
- throw new Error("No response body");
143
- }
144
-
145
- // Open file for writing
146
- partialFile = Bun.file(outputPath).writer();
147
-
148
- console.log(`Downloading ${fileName}...`);
149
- if (totalBytes > 0) {
150
- console.log(`Total size: ${(totalBytes / 1024 / 1024).toFixed(2)} MB`);
151
- }
152
-
153
- // Read chunks with cancellation checks
154
- while (true) {
155
- // Check for cancellation between chunks
156
- if (signal.aborted) {
157
-
158
- console.warn("Download cancelled by user");
159
- throw new Error("AbortError");
160
- }
161
-
162
- const { done, value } = await reader.read();
163
-
164
- if (done) {
165
- break;
166
- }
167
-
168
- // Write chunk
169
- partialFile.write(value);
170
- downloadedBytes += value.byteLength;
171
-
172
- // Log progress
173
- if (totalBytes > 0) {
174
- const percent = ((downloadedBytes / totalBytes) * 100).toFixed(1);
175
- const mbDownloaded = (downloadedBytes / 1024 / 1024).toFixed(2);
176
- const mbTotal = (totalBytes / 1024 / 1024).toFixed(2);
177
- Bun.write(Bun.stdout, `\rProgress: ${percent}% (${mbDownloaded}/${mbTotal} MB)`);
178
- } else {
179
- const mbDownloaded = (downloadedBytes / 1024 / 1024).toFixed(2);
180
- Bun.write(Bun.stdout, `\rDownloaded: ${mbDownloaded} MB`);
181
- }
182
- }
183
-
184
- // Finalize file
185
- await partialFile.end();
186
- console.log("\nDownload complete!");
187
-
188
- return {
189
- success: true,
190
- data: {
191
- file: outputPath,
192
- size: downloadedBytes,
193
- url: url.toString(),
194
- },
195
- message: `Downloaded ${fileName} (${(downloadedBytes / 1024 / 1024).toFixed(2)} MB)`,
196
- };
197
-
198
- } catch (error) {
199
- // Handle cancellation
200
- if (signal.aborted || (error as Error).name === "AbortError") {
201
-
202
- console.log("\nDownload cancelled.");
203
-
204
- // Cleanup partial file
205
- await this.cleanup(outputPath, partialFile);
206
-
207
- return {
208
- success: false,
209
- message: "Download cancelled by user",
210
- data: {
211
- downloadedBytes,
212
- cancelled: true,
213
- },
214
- };
215
- }
216
-
217
- // Handle other errors
218
- await this.cleanup(outputPath, partialFile);
219
- throw error;
220
- }
221
- }
222
-
223
- private async cleanup(
224
- outputPath: string,
225
- sink: Bun.FileSink | null
226
- ): Promise<void> {
227
- try {
228
- // Close file handle
229
- if (sink) {
230
- await sink.end();
231
- }
232
-
233
- // Remove partial file
234
- const file = Bun.file(outputPath);
235
- if (await file.exists()) {
236
- await Bun.write(outputPath, ""); // Clear file
237
- // In production: fs.unlinkSync(outputPath);
238
- console.log("Cleaned up partial download.");
239
- }
240
- } catch (e) {
241
- // Ignore cleanup errors
242
- }
243
- }
244
- }
245
- ```
246
-
247
- ## Step 4: Create the Application
248
-
249
- Create `src/index.ts`:
250
-
251
- ```typescript
252
- import { TuiApplication } from "@pablozaiden/terminatui";
253
- import { DownloadCommand } from "./commands/download";
254
-
255
- class DownloadManager extends TuiApplication {
256
- constructor() {
257
- super({
258
- name: "download-manager",
259
- version: "1.0.0",
260
- commands: [new DownloadCommand()],
261
- });
262
- }
263
- }
264
-
265
- await new DownloadManager().run();
266
- ```
267
-
268
- ## Step 6: Test Cancellation
269
-
270
- ```bash
271
- # Start a large download
272
- bun src/index.ts download https://speed.hetzner.de/100MB.bin --output ./test-downloads
273
-
274
- # While downloading, press Ctrl+C
275
- # Should see: "Download cancelled. Cleaned up partial download."
276
-
277
- # Run TUI mode
278
- bun src/index.ts --tui
279
-
280
- # Start download, then press Esc to cancel
281
- # Same cancellation behavior with cleanup
282
- ```
283
-
284
- ## Cancellation Patterns
285
-
286
- ### 1. Check Signal Before Long Operations
287
-
288
- ```typescript
289
- if (signal?.aborted) {
290
- return { success: false, message: "Cancelled" };
291
- }
292
- ```
293
-
294
- ### 2. Pass Signal to fetch/APIs
295
-
296
- ```typescript
297
- await fetch(url, { signal });
298
- await someAsyncApi({ signal });
299
- ```
300
-
301
- ### 3. Check Between Iterations
302
-
303
- ```typescript
304
- for (const item of items) {
305
- if (signal?.aborted) break;
306
- await processItem(item);
307
- }
308
- ```
309
-
310
- ### 4. Always Cleanup
311
-
312
- ```typescript
313
- try {
314
- // ... cancellable work ...
315
- } catch (error) {
316
- if (signal?.aborted) {
317
- await cleanup();
318
- return { success: false, message: "Cancelled" };
319
- }
320
- throw error;
321
- }
322
- ```
323
-
324
- ## What You Learned
325
-
326
- - Accept `CommandExecutionContext` with `AbortSignal`
327
- - Check `signal.aborted` between operations
328
- - Pass signal to `fetch` and other async APIs
329
- - Clean up resources on cancellation
330
- - Return meaningful results for cancelled operations
331
-
332
- ## Next Steps
333
-
334
- → [Guide 8: Building a Complete Application](08-complete-application.md)