@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.
Files changed (95) hide show
  1. package/.devcontainer/devcontainer.json +19 -0
  2. package/.devcontainer/install-prerequisites.sh +49 -0
  3. package/.github/workflows/copilot-setup-steps.yml +32 -0
  4. package/.github/workflows/pull-request.yml +27 -0
  5. package/.github/workflows/release-npm-package.yml +78 -0
  6. package/LICENSE +21 -0
  7. package/README.md +524 -0
  8. package/examples/tui-app/commands/greet.ts +75 -0
  9. package/examples/tui-app/commands/index.ts +3 -0
  10. package/examples/tui-app/commands/math.ts +114 -0
  11. package/examples/tui-app/commands/status.ts +75 -0
  12. package/examples/tui-app/index.ts +34 -0
  13. package/guides/01-hello-world.md +96 -0
  14. package/guides/02-adding-options.md +103 -0
  15. package/guides/03-multiple-commands.md +163 -0
  16. package/guides/04-subcommands.md +206 -0
  17. package/guides/05-interactive-tui.md +194 -0
  18. package/guides/06-config-validation.md +264 -0
  19. package/guides/07-async-cancellation.md +388 -0
  20. package/guides/08-complete-application.md +673 -0
  21. package/guides/README.md +74 -0
  22. package/package.json +32 -0
  23. package/src/__tests__/application.test.ts +425 -0
  24. package/src/__tests__/buildCliCommand.test.ts +125 -0
  25. package/src/__tests__/builtins.test.ts +133 -0
  26. package/src/__tests__/colors.test.ts +127 -0
  27. package/src/__tests__/command.test.ts +157 -0
  28. package/src/__tests__/commandClass.test.ts +130 -0
  29. package/src/__tests__/context.test.ts +97 -0
  30. package/src/__tests__/help.test.ts +412 -0
  31. package/src/__tests__/parser.test.ts +268 -0
  32. package/src/__tests__/registry.test.ts +195 -0
  33. package/src/__tests__/registryNew.test.ts +160 -0
  34. package/src/__tests__/schemaToFields.test.ts +176 -0
  35. package/src/__tests__/table.test.ts +146 -0
  36. package/src/__tests__/tui.test.ts +26 -0
  37. package/src/builtins/help.ts +85 -0
  38. package/src/builtins/index.ts +4 -0
  39. package/src/builtins/settings.ts +106 -0
  40. package/src/builtins/version.ts +72 -0
  41. package/src/cli/help.ts +174 -0
  42. package/src/cli/index.ts +3 -0
  43. package/src/cli/output/colors.ts +74 -0
  44. package/src/cli/output/index.ts +2 -0
  45. package/src/cli/output/table.ts +141 -0
  46. package/src/cli/parser.ts +241 -0
  47. package/src/commands/help.ts +50 -0
  48. package/src/commands/index.ts +1 -0
  49. package/src/components/index.ts +147 -0
  50. package/src/core/application.ts +461 -0
  51. package/src/core/command.ts +269 -0
  52. package/src/core/context.ts +112 -0
  53. package/src/core/help.ts +214 -0
  54. package/src/core/index.ts +15 -0
  55. package/src/core/logger.ts +164 -0
  56. package/src/core/registry.ts +140 -0
  57. package/src/hooks/index.ts +131 -0
  58. package/src/index.ts +137 -0
  59. package/src/registry/commandRegistry.ts +77 -0
  60. package/src/registry/index.ts +1 -0
  61. package/src/tui/TuiApp.tsx +582 -0
  62. package/src/tui/TuiApplication.tsx +230 -0
  63. package/src/tui/app.ts +29 -0
  64. package/src/tui/components/ActionButton.tsx +36 -0
  65. package/src/tui/components/CliModal.tsx +81 -0
  66. package/src/tui/components/CommandSelector.tsx +159 -0
  67. package/src/tui/components/ConfigForm.tsx +148 -0
  68. package/src/tui/components/EditorModal.tsx +177 -0
  69. package/src/tui/components/FieldRow.tsx +30 -0
  70. package/src/tui/components/Header.tsx +31 -0
  71. package/src/tui/components/JsonHighlight.tsx +128 -0
  72. package/src/tui/components/LogsPanel.tsx +86 -0
  73. package/src/tui/components/ResultsPanel.tsx +93 -0
  74. package/src/tui/components/StatusBar.tsx +59 -0
  75. package/src/tui/components/index.ts +13 -0
  76. package/src/tui/components/types.ts +30 -0
  77. package/src/tui/context/KeyboardContext.tsx +118 -0
  78. package/src/tui/context/index.ts +7 -0
  79. package/src/tui/hooks/index.ts +35 -0
  80. package/src/tui/hooks/useClipboard.ts +66 -0
  81. package/src/tui/hooks/useCommandExecutor.ts +131 -0
  82. package/src/tui/hooks/useConfigState.ts +171 -0
  83. package/src/tui/hooks/useKeyboardHandler.ts +91 -0
  84. package/src/tui/hooks/useLogStream.ts +96 -0
  85. package/src/tui/hooks/useSpinner.ts +46 -0
  86. package/src/tui/index.ts +65 -0
  87. package/src/tui/theme.ts +21 -0
  88. package/src/tui/utils/buildCliCommand.ts +90 -0
  89. package/src/tui/utils/index.ts +13 -0
  90. package/src/tui/utils/parameterPersistence.ts +96 -0
  91. package/src/tui/utils/schemaToFields.ts +144 -0
  92. package/src/types/command.ts +103 -0
  93. package/src/types/execution.ts +11 -0
  94. package/src/types/index.ts +1 -0
  95. package/tsconfig.json +25 -0
@@ -0,0 +1,673 @@
1
+ # Guide 8: Building a Complete Application (Complex)
2
+
3
+ Bring together everything to build a production-ready CLI application with multiple commands, services, logging, and a polished TUI.
4
+
5
+ ## What You'll Build
6
+
7
+ A task management CLI with:
8
+ - Multiple commands: list, add, complete, stats
9
+ - Shared services (database, notifications)
10
+ - Logging with configurable verbosity
11
+ - Full TUI support with custom result rendering
12
+ - Config validation and type safety
13
+
14
+ ```bash
15
+ tasks add "Write documentation" --priority high
16
+ tasks list --filter pending
17
+ tasks complete abc123
18
+ tasks stats
19
+ tasks --tui # Interactive mode
20
+ ```
21
+
22
+ ## Project Structure
23
+
24
+ ```
25
+ src/
26
+ ├── index.ts # Application entry
27
+ ├── services/
28
+ │ ├── database.ts # Task storage
29
+ │ └── notifications.ts # Task notifications
30
+ ├── commands/
31
+ │ ├── add.ts
32
+ │ ├── list.ts
33
+ │ ├── complete.ts
34
+ │ └── stats.ts
35
+ └── types.ts # Shared types
36
+ ```
37
+
38
+ ## Step 1: Define Shared Types
39
+
40
+ Create `src/types.ts`:
41
+
42
+ ```typescript
43
+ export interface Task {
44
+ id: string;
45
+ title: string;
46
+ priority: "low" | "medium" | "high";
47
+ status: "pending" | "completed";
48
+ createdAt: Date;
49
+ completedAt?: Date;
50
+ }
51
+
52
+ export interface TaskStats {
53
+ total: number;
54
+ pending: number;
55
+ completed: number;
56
+ byPriority: Record<string, number>;
57
+ }
58
+ ```
59
+
60
+ ## Step 2: Create Services
61
+
62
+ Create `src/services/database.ts`:
63
+
64
+ ```typescript
65
+ import type { Task, TaskStats } from "../types";
66
+
67
+ // In-memory database (replace with real DB in production)
68
+ const tasks: Map<string, Task> = new Map();
69
+
70
+ export class Database {
71
+ async addTask(title: string, priority: Task["priority"]): Promise<Task> {
72
+ const id = Math.random().toString(36).substring(7);
73
+ const task: Task = {
74
+ id,
75
+ title,
76
+ priority,
77
+ status: "pending",
78
+ createdAt: new Date(),
79
+ };
80
+ tasks.set(id, task);
81
+ return task;
82
+ }
83
+
84
+ async getTask(id: string): Promise<Task | undefined> {
85
+ return tasks.get(id);
86
+ }
87
+
88
+ async listTasks(filter?: "pending" | "completed" | "all"): Promise<Task[]> {
89
+ const all = Array.from(tasks.values());
90
+ if (!filter || filter === "all") return all;
91
+ return all.filter((t) => t.status === filter);
92
+ }
93
+
94
+ async completeTask(id: string): Promise<Task | undefined> {
95
+ const task = tasks.get(id);
96
+ if (task) {
97
+ task.status = "completed";
98
+ task.completedAt = new Date();
99
+ tasks.set(id, task);
100
+ }
101
+ return task;
102
+ }
103
+
104
+ async getStats(): Promise<TaskStats> {
105
+ const all = Array.from(tasks.values());
106
+ return {
107
+ total: all.length,
108
+ pending: all.filter((t) => t.status === "pending").length,
109
+ completed: all.filter((t) => t.status === "completed").length,
110
+ byPriority: {
111
+ high: all.filter((t) => t.priority === "high").length,
112
+ medium: all.filter((t) => t.priority === "medium").length,
113
+ low: all.filter((t) => t.priority === "low").length,
114
+ },
115
+ };
116
+ }
117
+ }
118
+ ```
119
+
120
+ Create `src/services/notifications.ts`:
121
+
122
+ ```typescript
123
+ import type { Task } from "../types";
124
+ import type { Logger } from "@pablozaiden/terminatui";
125
+
126
+ export class NotificationService {
127
+ constructor(private logger: Logger) {}
128
+
129
+ taskAdded(task: Task): void {
130
+ this.logger.info(`📝 Task added: ${task.title} [${task.priority}]`);
131
+ if (task.priority === "high") {
132
+ this.logger.warn(`⚠️ High priority task created!`);
133
+ }
134
+ }
135
+
136
+ taskCompleted(task: Task): void {
137
+ this.logger.info(`✅ Task completed: ${task.title}`);
138
+ const duration = task.completedAt!.getTime() - task.createdAt.getTime();
139
+ const hours = Math.floor(duration / 3600000);
140
+ if (hours > 24) {
141
+ this.logger.debug(`Task took ${hours} hours to complete`);
142
+ }
143
+ }
144
+ }
145
+ ```
146
+
147
+ ## Step 3: Create Commands
148
+
149
+ Create `src/commands/add.ts`:
150
+
151
+ ```typescript
152
+ import React from "react";
153
+ import { Text, Box } from "ink";
154
+ import {
155
+ Command,
156
+ ConfigValidationError,
157
+ type AppContext,
158
+ type OptionSchema,
159
+ type OptionValues,
160
+ type CommandResult,
161
+ } from "@pablozaiden/terminatui";
162
+ import { Database } from "../services/database";
163
+ import { NotificationService } from "../services/notifications";
164
+ import type { Task } from "../types";
165
+
166
+ const options = {
167
+ title: {
168
+ type: "string",
169
+ description: "Task title",
170
+ required: true,
171
+ label: "Task Title",
172
+ order: 1,
173
+ },
174
+ priority: {
175
+ type: "string",
176
+ description: "Task priority",
177
+ default: "medium",
178
+ enum: ["low", "medium", "high"],
179
+ label: "Priority",
180
+ order: 2,
181
+ },
182
+ } satisfies OptionSchema;
183
+
184
+ interface AddConfig {
185
+ title: string;
186
+ priority: Task["priority"];
187
+ }
188
+
189
+ export class AddCommand extends Command<typeof options, AddConfig> {
190
+ readonly name = "add";
191
+ readonly description = "Add a new task";
192
+ readonly options = options;
193
+ readonly displayName = "Add Task";
194
+ readonly actionLabel = "Create Task";
195
+ readonly group = "Tasks";
196
+ readonly order = 1;
197
+
198
+ private db = new Database();
199
+
200
+ override buildConfig(
201
+ _ctx: AppContext,
202
+ opts: OptionValues<typeof options>
203
+ ): AddConfig {
204
+ const title = (opts["title"] as string)?.trim();
205
+ if (!title) {
206
+ throw new ConfigValidationError("Task title cannot be empty", "title");
207
+ }
208
+ if (title.length > 200) {
209
+ throw new ConfigValidationError(
210
+ "Task title must be 200 characters or less",
211
+ "title"
212
+ );
213
+ }
214
+
215
+ const priority = opts["priority"] as Task["priority"] ?? "medium";
216
+
217
+ return { title, priority };
218
+ }
219
+
220
+ async execute(ctx: AppContext, config: AddConfig): Promise<CommandResult> {
221
+ ctx.logger.debug(`Creating task: ${config.title}`);
222
+
223
+ const task = await this.db.addTask(config.title, config.priority);
224
+
225
+ const notifications = new NotificationService(ctx.logger);
226
+ notifications.taskAdded(task);
227
+
228
+ return {
229
+ success: true,
230
+ data: task,
231
+ message: `Created task: ${task.title} (${task.id})`,
232
+ };
233
+ }
234
+
235
+ override renderResult(result: CommandResult): React.ReactNode {
236
+ if (!result.success) {
237
+ return <Text color="red">✗ {result.message}</Text>;
238
+ }
239
+
240
+ const task = result.data as Task;
241
+ const priorityColor = {
242
+ high: "red",
243
+ medium: "yellow",
244
+ low: "green",
245
+ }[task.priority] as "red" | "yellow" | "green";
246
+
247
+ return (
248
+ <Box flexDirection="column" gap={1}>
249
+ <Text color="green">✓ Task Created</Text>
250
+ <Box>
251
+ <Text dimColor>ID: </Text>
252
+ <Text>{task.id}</Text>
253
+ </Box>
254
+ <Box>
255
+ <Text dimColor>Title: </Text>
256
+ <Text>{task.title}</Text>
257
+ </Box>
258
+ <Box>
259
+ <Text dimColor>Priority: </Text>
260
+ <Text color={priorityColor}>{task.priority}</Text>
261
+ </Box>
262
+ </Box>
263
+ );
264
+ }
265
+
266
+ override getClipboardContent(result: CommandResult): string | undefined {
267
+ return result.data?.id;
268
+ }
269
+ }
270
+ ```
271
+
272
+ Create `src/commands/list.ts`:
273
+
274
+ ```typescript
275
+ import React from "react";
276
+ import { Text, Box } from "ink";
277
+ import {
278
+ Command,
279
+ type AppContext,
280
+ type OptionSchema,
281
+ type OptionValues,
282
+ type CommandResult,
283
+ } from "@pablozaiden/terminatui";
284
+ import { Database } from "../services/database";
285
+ import type { Task } from "../types";
286
+
287
+ const options = {
288
+ filter: {
289
+ type: "string",
290
+ description: "Filter by status",
291
+ default: "all",
292
+ enum: ["all", "pending", "completed"],
293
+ label: "Status Filter",
294
+ },
295
+ priority: {
296
+ type: "string",
297
+ description: "Filter by priority",
298
+ enum: ["low", "medium", "high"],
299
+ label: "Priority Filter",
300
+ },
301
+ } satisfies OptionSchema;
302
+
303
+ interface ListConfig {
304
+ filter: "all" | "pending" | "completed";
305
+ priority?: Task["priority"];
306
+ }
307
+
308
+ export class ListCommand extends Command<typeof options, ListConfig> {
309
+ readonly name = "list";
310
+ readonly description = "List tasks";
311
+ readonly options = options;
312
+ readonly displayName = "List Tasks";
313
+ readonly actionLabel = "Show Tasks";
314
+ readonly group = "Tasks";
315
+ readonly order = 2;
316
+
317
+ // Execute immediately when selected in TUI
318
+ readonly immediateExecution = true;
319
+
320
+ private db = new Database();
321
+
322
+ override buildConfig(
323
+ _ctx: AppContext,
324
+ opts: OptionValues<typeof options>
325
+ ): ListConfig {
326
+ return {
327
+ filter: (opts["filter"] as ListConfig["filter"]) ?? "all",
328
+ priority: opts["priority"] as Task["priority"] | undefined,
329
+ };
330
+ }
331
+
332
+ async execute(ctx: AppContext, config: ListConfig): Promise<CommandResult> {
333
+ ctx.logger.debug(`Listing tasks: filter=${config.filter}`);
334
+
335
+ let tasks = await this.db.listTasks(config.filter);
336
+
337
+ if (config.priority) {
338
+ tasks = tasks.filter((t) => t.priority === config.priority);
339
+ }
340
+
341
+ return {
342
+ success: true,
343
+ data: tasks,
344
+ message: `Found ${tasks.length} tasks`,
345
+ };
346
+ }
347
+
348
+ override renderResult(result: CommandResult): React.ReactNode {
349
+ const tasks = (result.data as Task[]) ?? [];
350
+
351
+ if (tasks.length === 0) {
352
+ return <Text dimColor>No tasks found</Text>;
353
+ }
354
+
355
+ const priorityColor = (p: string) =>
356
+ ({ high: "red", medium: "yellow", low: "green" })[p] as "red" | "yellow" | "green";
357
+
358
+ return (
359
+ <Box flexDirection="column" gap={1}>
360
+ <Text bold>Tasks ({tasks.length})</Text>
361
+ {tasks.map((task) => (
362
+ <Box key={task.id} gap={2}>
363
+ <Text dimColor>[{task.id}]</Text>
364
+ <Text color={task.status === "completed" ? "gray" : undefined}>
365
+ {task.status === "completed" ? "✓" : "○"} {task.title}
366
+ </Text>
367
+ <Text color={priorityColor(task.priority)}>{task.priority}</Text>
368
+ </Box>
369
+ ))}
370
+ </Box>
371
+ );
372
+ }
373
+
374
+ override getClipboardContent(result: CommandResult): string | undefined {
375
+ const tasks = result.data as Task[];
376
+ return tasks?.map((t) => `${t.id}: ${t.title}`).join("\n");
377
+ }
378
+ }
379
+ ```
380
+
381
+ Create `src/commands/complete.ts`:
382
+
383
+ ```typescript
384
+ import React from "react";
385
+ import { Text, Box } from "ink";
386
+ import {
387
+ Command,
388
+ ConfigValidationError,
389
+ type AppContext,
390
+ type OptionSchema,
391
+ type OptionValues,
392
+ type CommandResult,
393
+ } from "@pablozaiden/terminatui";
394
+ import { Database } from "../services/database";
395
+ import { NotificationService } from "../services/notifications";
396
+ import type { Task } from "../types";
397
+
398
+ const options = {
399
+ id: {
400
+ type: "string",
401
+ description: "Task ID to complete",
402
+ required: true,
403
+ label: "Task ID",
404
+ },
405
+ } satisfies OptionSchema;
406
+
407
+ interface CompleteConfig {
408
+ id: string;
409
+ }
410
+
411
+ export class CompleteCommand extends Command<typeof options, CompleteConfig> {
412
+ readonly name = "complete";
413
+ readonly description = "Mark a task as complete";
414
+ readonly options = options;
415
+ readonly displayName = "Complete Task";
416
+ readonly actionLabel = "Mark Complete";
417
+ readonly group = "Tasks";
418
+ readonly order = 3;
419
+
420
+ private db = new Database();
421
+
422
+ override buildConfig(
423
+ _ctx: AppContext,
424
+ opts: OptionValues<typeof options>
425
+ ): CompleteConfig {
426
+ const id = opts["id"] as string;
427
+ if (!id?.trim()) {
428
+ throw new ConfigValidationError("Task ID is required", "id");
429
+ }
430
+ return { id: id.trim() };
431
+ }
432
+
433
+ async execute(ctx: AppContext, config: CompleteConfig): Promise<CommandResult> {
434
+ ctx.logger.debug(`Completing task: ${config.id}`);
435
+
436
+ const task = await this.db.completeTask(config.id);
437
+
438
+ if (!task) {
439
+ return {
440
+ success: false,
441
+ message: `Task not found: ${config.id}`,
442
+ };
443
+ }
444
+
445
+ const notifications = new NotificationService(ctx.logger);
446
+ notifications.taskCompleted(task);
447
+
448
+ return {
449
+ success: true,
450
+ data: task,
451
+ message: `Completed: ${task.title}`,
452
+ };
453
+ }
454
+
455
+ override renderResult(result: CommandResult): React.ReactNode {
456
+ if (!result.success) {
457
+ return <Text color="red">✗ {result.message}</Text>;
458
+ }
459
+
460
+ const task = result.data as Task;
461
+ return (
462
+ <Box flexDirection="column">
463
+ <Text color="green">✓ Task Completed</Text>
464
+ <Text>{task.title}</Text>
465
+ <Text dimColor>
466
+ Completed at: {task.completedAt?.toLocaleString()}
467
+ </Text>
468
+ </Box>
469
+ );
470
+ }
471
+ }
472
+ ```
473
+
474
+ Create `src/commands/stats.ts`:
475
+
476
+ ```typescript
477
+ import React from "react";
478
+ import { Text, Box } from "ink";
479
+ import {
480
+ Command,
481
+ type AppContext,
482
+ type OptionSchema,
483
+ type CommandResult,
484
+ } from "@pablozaiden/terminatui";
485
+ import { Database } from "../services/database";
486
+ import type { TaskStats } from "../types";
487
+
488
+ const options = {} satisfies OptionSchema;
489
+
490
+ export class StatsCommand extends Command<typeof options> {
491
+ readonly name = "stats";
492
+ readonly description = "Show task statistics";
493
+ readonly options = options;
494
+ readonly displayName = "Statistics";
495
+ readonly actionLabel = "Show Stats";
496
+ readonly group = "Analytics";
497
+ readonly order = 1;
498
+ readonly immediateExecution = true;
499
+
500
+ private db = new Database();
501
+
502
+ async execute(ctx: AppContext): Promise<CommandResult> {
503
+ ctx.logger.debug("Fetching statistics");
504
+
505
+ const stats = await this.db.getStats();
506
+
507
+ return {
508
+ success: true,
509
+ data: stats,
510
+ message: `Total: ${stats.total}, Pending: ${stats.pending}, Completed: ${stats.completed}`,
511
+ };
512
+ }
513
+
514
+ override renderResult(result: CommandResult): React.ReactNode {
515
+ const stats = result.data as TaskStats;
516
+
517
+ const completionRate = stats.total > 0
518
+ ? Math.round((stats.completed / stats.total) * 100)
519
+ : 0;
520
+
521
+ return (
522
+ <Box flexDirection="column" gap={1}>
523
+ <Text bold>📊 Task Statistics</Text>
524
+
525
+ <Box flexDirection="column">
526
+ <Box gap={2}>
527
+ <Text dimColor>Total:</Text>
528
+ <Text>{stats.total}</Text>
529
+ </Box>
530
+ <Box gap={2}>
531
+ <Text dimColor>Pending:</Text>
532
+ <Text color="yellow">{stats.pending}</Text>
533
+ </Box>
534
+ <Box gap={2}>
535
+ <Text dimColor>Completed:</Text>
536
+ <Text color="green">{stats.completed}</Text>
537
+ </Box>
538
+ <Box gap={2}>
539
+ <Text dimColor>Completion Rate:</Text>
540
+ <Text color={completionRate >= 50 ? "green" : "yellow"}>
541
+ {completionRate}%
542
+ </Text>
543
+ </Box>
544
+ </Box>
545
+
546
+ <Box flexDirection="column" marginTop={1}>
547
+ <Text bold>By Priority:</Text>
548
+ <Box gap={2}>
549
+ <Text color="red">High: {stats.byPriority.high}</Text>
550
+ <Text color="yellow">Medium: {stats.byPriority.medium}</Text>
551
+ <Text color="green">Low: {stats.byPriority.low}</Text>
552
+ </Box>
553
+ </Box>
554
+ </Box>
555
+ );
556
+ }
557
+ }
558
+ ```
559
+
560
+ ## Step 4: Create the Application
561
+
562
+ Create `src/index.ts`:
563
+
564
+ ```typescript
565
+ import { TuiApplication } from "@pablozaiden/terminatui";
566
+ import { AddCommand } from "./commands/add";
567
+ import { ListCommand } from "./commands/list";
568
+ import { CompleteCommand } from "./commands/complete";
569
+ import { StatsCommand } from "./commands/stats";
570
+
571
+ class TasksCLI extends TuiApplication {
572
+ constructor() {
573
+ super({
574
+ name: "tasks",
575
+ version: "1.0.0",
576
+ description: "A simple task management CLI",
577
+ commands: [
578
+ new AddCommand(),
579
+ new ListCommand(),
580
+ new CompleteCommand(),
581
+ new StatsCommand(),
582
+ ],
583
+ });
584
+ }
585
+
586
+ // Optional: Override onBeforeRun for setup
587
+ protected override async onBeforeRun(): Promise<void> {
588
+ this.logger.debug("Tasks CLI starting...");
589
+ // Initialize database connections, load config, etc.
590
+ }
591
+
592
+ // Optional: Override onAfterRun for cleanup
593
+ protected override async onAfterRun(): Promise<void> {
594
+ this.logger.debug("Tasks CLI shutting down...");
595
+ // Close database connections, save state, etc.
596
+ }
597
+ }
598
+
599
+ await new TasksCLI().run();
600
+ ```
601
+
602
+ ## Step 5: Configure Package
603
+
604
+ Update `package.json`:
605
+
606
+ ```json
607
+ {
608
+ "name": "tasks-cli",
609
+ "version": "1.0.0",
610
+ "type": "module",
611
+ "bin": {
612
+ "tasks": "./src/index.ts"
613
+ },
614
+ "scripts": {
615
+ "start": "bun src/index.ts",
616
+ "tui": "bun src/index.ts --tui",
617
+ "build": "bun build src/index.ts --outdir dist --target bun"
618
+ },
619
+ "dependencies": {
620
+ "@pablozaiden/terminatui": "^1.0.0"
621
+ }
622
+ }
623
+ ```
624
+
625
+ ## Step 6: Run the Application
626
+
627
+ ```bash
628
+ # CLI Mode
629
+ bun start add "Write documentation" --priority high
630
+ bun start list --filter pending
631
+ bun start complete abc123
632
+ bun start stats
633
+
634
+ # TUI Mode
635
+ bun tui
636
+
637
+ # With verbose logging
638
+ bun start --verbose add "Debug task" --priority low
639
+ ```
640
+
641
+ ## TUI Feature Summary
642
+
643
+ | Feature | Implementation |
644
+ |---------|----------------|
645
+ | Command Groups | `group = "Tasks"` or `"Analytics"` |
646
+ | Command Order | `order = 1, 2, 3` |
647
+ | Display Names | `displayName = "Add Task"` |
648
+ | Action Labels | `actionLabel = "Create Task"` |
649
+ | Immediate Execution | `immediateExecution = true` |
650
+ | Custom Results | `renderResult()` |
651
+ | Clipboard Support | `getClipboardContent()` |
652
+
653
+ ## What You Learned
654
+
655
+ - **Project Structure**: Organize code into commands, services, types
656
+ - **Shared Services**: Database and notification services
657
+ - **Command Groups**: Organize commands in TUI sidebar
658
+ - **Full TUI Integration**: Custom rendering, clipboard, immediate execution
659
+ - **Lifecycle Hooks**: `onBeforeRun` and `onAfterRun`
660
+ - **Production Patterns**: Error handling, validation, logging
661
+
662
+ ## Complete Series Summary
663
+
664
+ 1. **Hello World** - Basic command structure
665
+ 2. **Adding Options** - Option types and defaults
666
+ 3. **Multiple Commands** - Command organization
667
+ 4. **Subcommands** - Nested command hierarchies
668
+ 5. **Interactive TUI** - TuiApplication basics
669
+ 6. **Config Validation** - buildConfig and validation
670
+ 7. **Async Cancellation** - Cancellable operations
671
+ 8. **Complete Application** - Full production app
672
+
673
+ You now have all the tools to build powerful CLI applications with terminatui! 🎉