@pablozaiden/terminatui 0.2.0 → 0.3.0-beta-1

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 (175) hide show
  1. package/AGENTS.md +14 -2
  2. package/CLAUDE.md +1 -0
  3. package/README.md +64 -43
  4. package/bun.lock +85 -0
  5. package/examples/tui-app/commands/config/app/get.ts +6 -10
  6. package/examples/tui-app/commands/config/app/index.ts +2 -6
  7. package/examples/tui-app/commands/config/app/set.ts +23 -13
  8. package/examples/tui-app/commands/config/index.ts +2 -6
  9. package/examples/tui-app/commands/config/user/get.ts +6 -10
  10. package/examples/tui-app/commands/config/user/index.ts +2 -6
  11. package/examples/tui-app/commands/config/user/set.ts +6 -10
  12. package/examples/tui-app/commands/greet.ts +13 -11
  13. package/examples/tui-app/commands/math.ts +5 -9
  14. package/examples/tui-app/commands/status.ts +21 -12
  15. package/examples/tui-app/index.ts +6 -3
  16. package/guides/01-hello-world.md +7 -2
  17. package/guides/02-adding-options.md +2 -2
  18. package/guides/03-multiple-commands.md +6 -8
  19. package/guides/04-subcommands.md +8 -8
  20. package/guides/05-interactive-tui.md +45 -30
  21. package/guides/06-config-validation.md +4 -12
  22. package/guides/07-async-cancellation.md +14 -16
  23. package/guides/08-complete-application.md +12 -42
  24. package/guides/README.md +7 -3
  25. package/package.json +4 -8
  26. package/src/__tests__/application.test.ts +87 -68
  27. package/src/__tests__/buildCliCommand.test.ts +99 -119
  28. package/src/__tests__/builtins.test.ts +27 -75
  29. package/src/__tests__/command.test.ts +100 -131
  30. package/src/__tests__/context.test.ts +1 -26
  31. package/src/__tests__/helpCore.test.ts +227 -0
  32. package/src/__tests__/parser.test.ts +98 -244
  33. package/src/__tests__/registry.test.ts +33 -160
  34. package/src/__tests__/schemaToFields.test.ts +75 -158
  35. package/src/builtins/help.ts +12 -4
  36. package/src/builtins/settings.ts +18 -32
  37. package/src/builtins/version.ts +4 -4
  38. package/src/cli/output/colors.ts +1 -1
  39. package/src/cli/parser.ts +26 -95
  40. package/src/core/application.ts +192 -110
  41. package/src/core/command.ts +26 -9
  42. package/src/core/context.ts +31 -20
  43. package/src/core/help.ts +24 -18
  44. package/src/core/knownCommands.ts +13 -0
  45. package/src/core/logger.ts +39 -42
  46. package/src/core/registry.ts +5 -12
  47. package/src/tui/TuiApplication.tsx +63 -120
  48. package/src/tui/TuiRoot.tsx +135 -0
  49. package/src/tui/adapters/factory.ts +19 -0
  50. package/src/tui/adapters/ink/InkRenderer.tsx +135 -0
  51. package/src/tui/adapters/ink/components/Button.tsx +12 -0
  52. package/src/tui/adapters/ink/components/Code.tsx +6 -0
  53. package/src/tui/adapters/ink/components/CodeHighlight.tsx +6 -0
  54. package/src/tui/adapters/ink/components/Container.tsx +5 -0
  55. package/src/tui/adapters/ink/components/Field.tsx +12 -0
  56. package/src/tui/adapters/ink/components/Label.tsx +24 -0
  57. package/src/tui/adapters/ink/components/MenuButton.tsx +12 -0
  58. package/src/tui/adapters/ink/components/MenuItem.tsx +17 -0
  59. package/src/tui/adapters/ink/components/Overlay.tsx +5 -0
  60. package/src/tui/adapters/ink/components/Panel.tsx +15 -0
  61. package/src/tui/adapters/ink/components/ScrollView.tsx +5 -0
  62. package/src/tui/adapters/ink/components/Select.tsx +44 -0
  63. package/src/tui/adapters/ink/components/Spacer.tsx +15 -0
  64. package/src/tui/adapters/ink/components/Spinner.tsx +5 -0
  65. package/src/tui/adapters/ink/components/TextInput.tsx +22 -0
  66. package/src/tui/adapters/ink/components/Value.tsx +7 -0
  67. package/src/tui/adapters/ink/keyboard.ts +97 -0
  68. package/src/tui/adapters/ink/utils.ts +16 -0
  69. package/src/tui/adapters/opentui/OpenTuiRenderer.tsx +115 -0
  70. package/src/tui/adapters/opentui/components/Button.tsx +13 -0
  71. package/src/tui/adapters/opentui/components/Code.tsx +12 -0
  72. package/src/tui/adapters/opentui/components/CodeHighlight.tsx +24 -0
  73. package/src/tui/adapters/opentui/components/Container.tsx +56 -0
  74. package/src/tui/adapters/opentui/components/Field.tsx +18 -0
  75. package/src/tui/adapters/opentui/components/Label.tsx +15 -0
  76. package/src/tui/adapters/opentui/components/MenuButton.tsx +14 -0
  77. package/src/tui/adapters/opentui/components/MenuItem.tsx +29 -0
  78. package/src/tui/adapters/opentui/components/Overlay.tsx +21 -0
  79. package/src/tui/adapters/opentui/components/Panel.tsx +78 -0
  80. package/src/tui/adapters/opentui/components/ScrollView.tsx +85 -0
  81. package/src/tui/adapters/opentui/components/Select.tsx +59 -0
  82. package/src/tui/adapters/opentui/components/Spacer.tsx +5 -0
  83. package/src/tui/adapters/opentui/components/Spinner.tsx +12 -0
  84. package/src/tui/adapters/opentui/components/TextInput.tsx +13 -0
  85. package/src/tui/adapters/opentui/components/Value.tsx +13 -0
  86. package/src/tui/{hooks → adapters/opentui/hooks}/useSpinner.ts +2 -11
  87. package/src/tui/adapters/opentui/keyboard.ts +61 -0
  88. package/src/tui/adapters/types.ts +70 -0
  89. package/src/tui/components/ActionButton.tsx +0 -36
  90. package/src/tui/components/CommandSelector.tsx +45 -92
  91. package/src/tui/components/ConfigForm.tsx +68 -42
  92. package/src/tui/components/FieldRow.tsx +0 -30
  93. package/src/tui/components/Header.tsx +14 -13
  94. package/src/tui/components/JsonHighlight.tsx +10 -17
  95. package/src/tui/components/ModalBase.tsx +38 -0
  96. package/src/tui/components/ResultsPanel.tsx +27 -36
  97. package/src/tui/components/StatusBar.tsx +24 -39
  98. package/src/tui/components/logColors.ts +12 -0
  99. package/src/tui/context/ClipboardContext.tsx +87 -0
  100. package/src/tui/context/ExecutorContext.tsx +139 -0
  101. package/src/tui/context/KeyboardContext.tsx +85 -71
  102. package/src/tui/context/LogsContext.tsx +35 -0
  103. package/src/tui/context/NavigationContext.tsx +194 -0
  104. package/src/tui/context/RendererContext.tsx +20 -0
  105. package/src/tui/context/TuiAppContext.tsx +58 -0
  106. package/src/tui/hooks/useActiveKeyHandler.ts +75 -0
  107. package/src/tui/hooks/useBackHandler.ts +34 -0
  108. package/src/tui/hooks/useClipboard.ts +40 -25
  109. package/src/tui/hooks/useClipboardProvider.ts +42 -0
  110. package/src/tui/hooks/useGlobalKeyHandler.ts +54 -0
  111. package/src/tui/modals/CliModal.tsx +82 -0
  112. package/src/tui/modals/EditorModal.tsx +207 -0
  113. package/src/tui/modals/LogsModal.tsx +98 -0
  114. package/src/tui/registry.ts +102 -0
  115. package/src/tui/screens/CommandSelectScreen.tsx +162 -0
  116. package/src/tui/screens/ConfigScreen.tsx +160 -0
  117. package/src/tui/screens/ErrorScreen.tsx +58 -0
  118. package/src/tui/screens/ResultsScreen.tsx +60 -0
  119. package/src/tui/screens/RunningScreen.tsx +72 -0
  120. package/src/tui/screens/ScreenBase.ts +6 -0
  121. package/src/tui/semantic/Button.tsx +7 -0
  122. package/src/tui/semantic/Code.tsx +7 -0
  123. package/src/tui/semantic/CodeHighlight.tsx +7 -0
  124. package/src/tui/semantic/Container.tsx +7 -0
  125. package/src/tui/semantic/Field.tsx +7 -0
  126. package/src/tui/semantic/Label.tsx +7 -0
  127. package/src/tui/semantic/MenuButton.tsx +7 -0
  128. package/src/tui/semantic/MenuItem.tsx +7 -0
  129. package/src/tui/semantic/Overlay.tsx +7 -0
  130. package/src/tui/semantic/Panel.tsx +7 -0
  131. package/src/tui/semantic/ScrollView.tsx +9 -0
  132. package/src/tui/semantic/Select.tsx +7 -0
  133. package/src/tui/semantic/Spacer.tsx +7 -0
  134. package/src/tui/semantic/Spinner.tsx +7 -0
  135. package/src/tui/semantic/TextInput.tsx +7 -0
  136. package/src/tui/semantic/Value.tsx +7 -0
  137. package/src/tui/semantic/types.ts +195 -0
  138. package/src/tui/theme.ts +25 -14
  139. package/src/tui/utils/buildCliCommand.ts +1 -0
  140. package/src/tui/utils/getEnumKeys.ts +3 -0
  141. package/src/tui/utils/parameterPersistence.ts +1 -0
  142. package/src/types/command.ts +0 -60
  143. package/examples/tui-app/commands/index.ts +0 -4
  144. package/src/__tests__/colors.test.ts +0 -127
  145. package/src/__tests__/commandClass.test.ts +0 -130
  146. package/src/__tests__/help.test.ts +0 -412
  147. package/src/__tests__/registryNew.test.ts +0 -160
  148. package/src/__tests__/table.test.ts +0 -146
  149. package/src/__tests__/tui.test.ts +0 -26
  150. package/src/builtins/index.ts +0 -4
  151. package/src/cli/help.ts +0 -174
  152. package/src/cli/index.ts +0 -3
  153. package/src/cli/output/index.ts +0 -2
  154. package/src/cli/output/table.ts +0 -141
  155. package/src/commands/help.ts +0 -50
  156. package/src/commands/index.ts +0 -1
  157. package/src/components/index.ts +0 -147
  158. package/src/core/index.ts +0 -15
  159. package/src/hooks/index.ts +0 -131
  160. package/src/index.ts +0 -137
  161. package/src/registry/commandRegistry.ts +0 -77
  162. package/src/registry/index.ts +0 -1
  163. package/src/tui/TuiApp.tsx +0 -619
  164. package/src/tui/app.ts +0 -29
  165. package/src/tui/components/CliModal.tsx +0 -81
  166. package/src/tui/components/EditorModal.tsx +0 -177
  167. package/src/tui/components/LogsPanel.tsx +0 -86
  168. package/src/tui/components/index.ts +0 -13
  169. package/src/tui/context/index.ts +0 -7
  170. package/src/tui/hooks/index.ts +0 -35
  171. package/src/tui/hooks/useKeyboardHandler.ts +0 -91
  172. package/src/tui/hooks/useLogStream.ts +0 -96
  173. package/src/tui/index.ts +0 -65
  174. package/src/tui/utils/index.ts +0 -13
  175. package/src/types/index.ts +0 -1
@@ -1,10 +1,6 @@
1
- import {
2
- Command,
3
- type AppContext,
4
- type OptionSchema,
5
- type OptionValues,
6
- type CommandResult
7
- } from "../../../../../src/index.ts";
1
+ import { Command, type CommandResult } from "../../../../../src/core/command";
2
+ import { AppContext } from "../../../../../src/core/context";
3
+ import type { OptionSchema, OptionValues } from "../../../../../src/types/command";
8
4
 
9
5
  const options = {
10
6
  key: {
@@ -31,7 +27,7 @@ export class UserGetCommand extends Command<typeof options> {
31
27
  { command: "config user get --key email", description: "Get user email" },
32
28
  ];
33
29
 
34
- override async execute(ctx: AppContext, opts: OptionValues<typeof options>): Promise<CommandResult> {
30
+ override async execute(opts: OptionValues<typeof options>): Promise<CommandResult> {
35
31
  // Simulated user config store
36
32
  const userConfig: Record<string, string> = {
37
33
  name: "John Doe",
@@ -43,14 +39,14 @@ export class UserGetCommand extends Command<typeof options> {
43
39
  const value = userConfig[opts.key];
44
40
 
45
41
  if (value === undefined) {
46
- ctx.logger.warn(`Key "${opts.key}" not found in user configuration`);
42
+ AppContext.current.logger.warn(`Key "${opts.key}" not found in user configuration`);
47
43
  return {
48
44
  success: false,
49
45
  message: `Key "${opts.key}" not found`,
50
46
  };
51
47
  }
52
48
 
53
- ctx.logger.info(`Retrieved user.${opts.key} = ${value}`);
49
+ AppContext.current.logger.info(`Retrieved user.${opts.key} = ${value}`);
54
50
  return {
55
51
  success: true,
56
52
  data: { key: opts.key, value },
@@ -1,8 +1,4 @@
1
- import {
2
- Command,
3
- type AppContext,
4
- type CommandResult
5
- } from "../../../../../src/index.ts";
1
+ import { Command, type CommandResult } from "../../../../../src/core/command.ts";
6
2
  import { UserGetCommand } from "./get.ts";
7
3
  import { UserSetCommand } from "./set.ts";
8
4
 
@@ -17,7 +13,7 @@ export class UserConfigCommand extends Command {
17
13
  new UserSetCommand(),
18
14
  ];
19
15
 
20
- override execute(_ctx: AppContext): CommandResult {
16
+ override execute(): CommandResult {
21
17
  console.log("Use 'config user <command>' for user configuration.");
22
18
  console.log("Available: get, set");
23
19
  return { success: true };
@@ -1,10 +1,6 @@
1
- import {
2
- Command,
3
- type AppContext,
4
- type OptionSchema,
5
- type OptionValues,
6
- type CommandResult
7
- } from "../../../../../src/index.ts";
1
+ import { Command, type CommandResult } from "../../../../../src/core/command";
2
+ import { AppContext } from "../../../../../src/core/context";
3
+ import type { OptionSchema, OptionValues } from "../../../../../src/types/command";
8
4
 
9
5
  const options = {
10
6
  key: {
@@ -40,13 +36,13 @@ export class UserSetCommand extends Command<typeof options> {
40
36
  { command: "config user set --key theme --value light", description: "Set theme" },
41
37
  ];
42
38
 
43
- override async execute(ctx: AppContext, opts: OptionValues<typeof options>): Promise<CommandResult> {
44
- ctx.logger.info(`Setting user.${opts.key} = "${opts.value}"`);
39
+ override async execute(opts: OptionValues<typeof options>): Promise<CommandResult> {
40
+ AppContext.current.logger.info(`Setting user.${opts.key} = "${opts.value}"`);
45
41
 
46
42
  // Simulate setting the value
47
43
  await new Promise(resolve => setTimeout(resolve, 300));
48
44
 
49
- ctx.logger.info(`Successfully updated user configuration`);
45
+ AppContext.current.logger.info(`Successfully updated user configuration`);
50
46
  return {
51
47
  success: true,
52
48
  data: { key: opts.key, value: opts.value },
@@ -1,10 +1,8 @@
1
- import {
2
- Command,
3
- type AppContext,
4
- type OptionSchema,
5
- type OptionValues,
6
- type CommandResult
7
- } from "../../../src/index.ts";
1
+ import type { ReactNode } from "react";
2
+ import { Command, type CommandResult } from "../../../src/core/command";
3
+ import { AppContext } from "../../../src/core/context";
4
+ import type { OptionSchema, OptionValues } from "../../../src/types/command";
5
+ import { JsonHighlight } from "../../../src/tui/components/JsonHighlight.tsx";
8
6
 
9
7
  const greetOptions = {
10
8
  name: {
@@ -20,7 +18,7 @@ const greetOptions = {
20
18
  type: "boolean",
21
19
  description: "Use uppercase",
22
20
  alias: "l",
23
- default: false,
21
+ default: true,
24
22
  label: "Loud Mode",
25
23
  order: 2,
26
24
  group: "Options",
@@ -48,16 +46,20 @@ export class GreetCommand extends Command<typeof greetOptions> {
48
46
  { command: "greet --name World --loud --times 3", description: "Loud greeting 3 times" },
49
47
  ];
50
48
 
51
- override async execute(ctx: AppContext, opts: OptionValues<typeof greetOptions>): Promise<CommandResult> {
49
+ override async execute(opts: OptionValues<typeof greetOptions>): Promise<CommandResult> {
52
50
  const greeting = this.createGreeting(opts);
53
- ctx.logger.info(greeting);
51
+ AppContext.current.logger.trace(greeting);
54
52
  return {
55
53
  success: true,
56
- data: { greeting, timestamp: new Date().toISOString() },
54
+ data: { greeting, timestamp: new Date().toISOString(), meta: { loud: opts.loud, times: opts.times } },
57
55
  message: greeting,
58
56
  };
59
57
  }
60
58
 
59
+ override renderResult(result: CommandResult): ReactNode {
60
+ return JsonHighlight({ value: result.data });
61
+ }
62
+
61
63
  override getClipboardContent(result: CommandResult): string | undefined {
62
64
  const data = result.data as { greeting?: string } | undefined;
63
65
  return data?.greeting;
@@ -1,10 +1,6 @@
1
- import {
2
- Command,
3
- type AppContext,
4
- type OptionSchema,
5
- type OptionValues,
6
- type CommandResult
7
- } from "../../../src/index.ts";
1
+ import { Command, type CommandResult } from "../../../src/core/command";
2
+ import { AppContext } from "../../../src/core/context";
3
+ import type { OptionSchema, OptionValues } from "../../../src/types/command";
8
4
 
9
5
  const mathOptions = {
10
6
  operation: {
@@ -50,10 +46,10 @@ export class MathCommand extends Command<typeof mathOptions> {
50
46
 
51
47
  override readonly actionLabel = "Calculate";
52
48
 
53
- override async execute(ctx: AppContext, opts: OptionValues<typeof mathOptions>): Promise<CommandResult> {
49
+ override async execute(opts: OptionValues<typeof mathOptions>): Promise<CommandResult> {
54
50
  const result = this.calculate(opts);
55
51
  if (!result.success) {
56
- ctx.logger.error(result.message || "Calculation failed");
52
+ AppContext.current.logger.error(result.message || "Calculation failed");
57
53
  }
58
54
  return result;
59
55
  }
@@ -1,10 +1,6 @@
1
- import {
2
- Command,
3
- type AppContext,
4
- type OptionSchema,
5
- type OptionValues,
6
- type CommandResult
7
- } from "../../../src/index.ts";
1
+ import { Command, type CommandExecutionContext, type CommandResult } from "../../../src/core/command";
2
+ import { AppContext } from "../../../src/core/context";
3
+ import type { OptionSchema, OptionValues } from "../../../src/types/command";
8
4
 
9
5
  const statusOptions = {
10
6
  detailed: {
@@ -26,9 +22,9 @@ export class StatusCommand extends Command<typeof statusOptions> {
26
22
  override readonly actionLabel = "Check Status";
27
23
  override readonly immediateExecution = true; // No required fields
28
24
 
29
- override async execute(ctx: AppContext, opts: OptionValues<typeof statusOptions>): Promise<CommandResult> {
30
- const result = await this.getStatus(opts);
31
- ctx.logger.info(result.message || "Status check complete");
25
+ override async execute(opts: OptionValues<typeof statusOptions>, execCtx : CommandExecutionContext): Promise<CommandResult> {
26
+ const result = await this.getStatus(opts, execCtx);
27
+ AppContext.current.logger.info(result.message || "Status check complete");
32
28
  return result;
33
29
  }
34
30
 
@@ -45,11 +41,24 @@ export class StatusCommand extends Command<typeof statusOptions> {
45
41
  ].join("\n");
46
42
  }
47
43
 
48
- private async getStatus(opts: OptionValues<typeof statusOptions>): Promise<CommandResult> {
44
+ private async getStatus(opts: OptionValues<typeof statusOptions>, execCtx: CommandExecutionContext): Promise<CommandResult> {
49
45
  const detailed = opts.detailed as boolean;
50
46
 
51
47
  // Simulate some async work
52
- await new Promise((resolve) => setTimeout(resolve, 500));
48
+ await new Promise(resolve => {
49
+ let count = 0;
50
+ let interval = setInterval(() => {
51
+ count++;
52
+ AppContext.current.logger.info(`Applying configuration... (${count}/5)`);
53
+ if (count >= 5 || execCtx.signal.aborted) {
54
+ if (count < 5) {
55
+ AppContext.current.logger.warn("Status check aborted");
56
+ }
57
+ clearInterval(interval);
58
+ resolve(undefined);
59
+ }
60
+ }, 1000);
61
+ });
53
62
 
54
63
  const memMB = Math.round(process.memoryUsage().heapUsed / 1024 / 1024);
55
64
  const uptimeSec = Math.round(process.uptime());
@@ -12,8 +12,11 @@
12
12
  * bun examples/tui-app/index.ts greet --name "World" --loud
13
13
  */
14
14
 
15
- import { TuiApplication } from "../../src/index.ts";
16
- import { GreetCommand, MathCommand, StatusCommand, ConfigCommand } from "./commands/index.ts";
15
+ import { TuiApplication } from "../../src/tui/TuiApplication.tsx";
16
+ import { ConfigCommand } from "./commands/config/index.ts";
17
+ import { GreetCommand } from "./commands/greet.ts";
18
+ import { MathCommand } from "./commands/math.ts";
19
+ import { StatusCommand } from "./commands/status.ts";
17
20
 
18
21
  class ExampleApp extends TuiApplication {
19
22
  constructor() {
@@ -32,4 +35,4 @@ class ExampleApp extends TuiApplication {
32
35
  }
33
36
 
34
37
  // Run the app
35
- await new ExampleApp().run(Bun.argv.slice(2));
38
+ await new ExampleApp().run();
@@ -28,7 +28,12 @@ bun add @pablozaiden/terminatui
28
28
  Create `src/greet.ts`:
29
29
 
30
30
  ```typescript
31
- import { Command, type AppContext, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
31
+ import {
32
+ Command,
33
+ type OptionSchema,
34
+ type CommandResult,
35
+ type CommandExecutionContext,
36
+ } from "@pablozaiden/terminatui";
32
37
 
33
38
  const options = {
34
39
  name: {
@@ -43,7 +48,7 @@ export class GreetCommand extends Command<typeof options> {
43
48
  readonly description = "Greet someone";
44
49
  readonly options = options;
45
50
 
46
- execute(_ctx: AppContext, config: { name: string }): CommandResult {
51
+ execute(config: { name: string }, _execCtx: CommandExecutionContext): CommandResult {
47
52
  console.log(`Hello, ${config.name}!`);
48
53
  return { success: true };
49
54
  }
@@ -19,7 +19,7 @@ myapp greet --name Alice --loud
19
19
  Update `src/greet.ts`:
20
20
 
21
21
  ```typescript
22
- import { Command, type AppContext, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
22
+ import { Command, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
23
23
 
24
24
  const options = {
25
25
  name: {
@@ -40,7 +40,7 @@ export class GreetCommand extends Command<typeof options> {
40
40
  readonly description = "Greet someone";
41
41
  readonly options = options;
42
42
 
43
- execute(_ctx: AppContext, config: { name: string; loud: boolean }): CommandResult {
43
+ execute(config: { name: string; loud: boolean }): CommandResult {
44
44
  const message = `Hello, ${config.name}!`;
45
45
  console.log(config.loud ? message.toUpperCase() : message);
46
46
  return { success: true };
@@ -19,8 +19,7 @@ fileutil count --dir ./src --ext .ts
19
19
  Create `src/commands/list.ts`:
20
20
 
21
21
  ```typescript
22
- import { readdirSync } from "fs";
23
- import { Command, type AppContext, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
22
+ import { Command, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
24
23
 
25
24
  const options = {
26
25
  dir: {
@@ -36,9 +35,9 @@ export class ListCommand extends Command<typeof options> {
36
35
  readonly description = "List files in a directory";
37
36
  readonly options = options;
38
37
 
39
- execute(_ctx: AppContext, config: { dir: string }): CommandResult {
38
+ async execute(config: { dir: string }): Promise<CommandResult> {
40
39
  try {
41
- const files = readdirSync(config.dir);
40
+ const files = await Array.fromAsync(new Bun.Glob("*").scan({ cwd: config.dir, onlyFiles: false }));
42
41
  console.log(`Files in ${config.dir}:`);
43
42
  files.forEach((file) => console.log(` ${file}`));
44
43
  return { success: true, message: `Found ${files.length} files` };
@@ -54,8 +53,7 @@ export class ListCommand extends Command<typeof options> {
54
53
  Create `src/commands/count.ts`:
55
54
 
56
55
  ```typescript
57
- import { readdirSync } from "fs";
58
- import { Command, type AppContext, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
56
+ import { Command, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
59
57
 
60
58
  const options = {
61
59
  dir: {
@@ -76,9 +74,9 @@ export class CountCommand extends Command<typeof options> {
76
74
  readonly description = "Count files in a directory";
77
75
  readonly options = options;
78
76
 
79
- execute(_ctx: AppContext, config: { dir: string; ext?: string }): CommandResult {
77
+ async execute(config: { dir: string; ext?: string }): Promise<CommandResult> {
80
78
  try {
81
- let files = readdirSync(config.dir);
79
+ let files = await Array.fromAsync(new Bun.Glob("*").scan({ cwd: config.dir, onlyFiles: false }));
82
80
 
83
81
  if (config.ext) {
84
82
  files = files.filter((f) => f.endsWith(config.ext!));
@@ -17,7 +17,7 @@ dbctl db status
17
17
  Create `src/commands/db/migrate.ts`:
18
18
 
19
19
  ```typescript
20
- import { Command, type AppContext, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
20
+ import { Command, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
21
21
 
22
22
  const options = {
23
23
  target: {
@@ -37,7 +37,7 @@ export class MigrateCommand extends Command<typeof options> {
37
37
  readonly description = "Run database migrations";
38
38
  readonly options = options;
39
39
 
40
- execute(ctx: AppContext, config: { target: string; dry: boolean }): CommandResult {
40
+ execute(config: { target: string; dry: boolean }): CommandResult {
41
41
  ctx.logger.info(`Migrating to: ${config.target}`);
42
42
 
43
43
  if (config.dry) {
@@ -55,7 +55,7 @@ export class MigrateCommand extends Command<typeof options> {
55
55
  Create `src/commands/db/seed.ts`:
56
56
 
57
57
  ```typescript
58
- import { Command, type AppContext, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
58
+ import { Command, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
59
59
 
60
60
  const options = {
61
61
  file: {
@@ -71,7 +71,7 @@ export class SeedCommand extends Command<typeof options> {
71
71
  readonly description = "Seed the database with data";
72
72
  readonly options = options;
73
73
 
74
- execute(ctx: AppContext, config: { file: string }): CommandResult {
74
+ execute(config: { file: string }): CommandResult {
75
75
  ctx.logger.info(`Seeding from: ${config.file}`);
76
76
  console.log(`Loading seed data from ${config.file}...`);
77
77
  console.log("Database seeded successfully!");
@@ -83,14 +83,14 @@ export class SeedCommand extends Command<typeof options> {
83
83
  Create `src/commands/db/status.ts`:
84
84
 
85
85
  ```typescript
86
- import { Command, type AppContext, type CommandResult } from "@pablozaiden/terminatui";
86
+ import { Command, type CommandResult } from "@pablozaiden/terminatui";
87
87
 
88
88
  export class StatusCommand extends Command {
89
89
  readonly name = "status";
90
90
  readonly description = "Show database status";
91
91
  readonly options = {};
92
92
 
93
- execute(_ctx: AppContext): CommandResult {
93
+ execute(): CommandResult {
94
94
  console.log("Database Status:");
95
95
  console.log(" Connected: Yes");
96
96
  console.log(" Version: 1.2.3");
@@ -105,7 +105,7 @@ export class StatusCommand extends Command {
105
105
  Create `src/commands/db/index.ts`:
106
106
 
107
107
  ```typescript
108
- import { Command, type AppContext, type CommandResult } from "@pablozaiden/terminatui";
108
+ import { Command, type CommandResult } from "@pablozaiden/terminatui";
109
109
  import { MigrateCommand } from "./migrate";
110
110
  import { SeedCommand } from "./seed";
111
111
  import { StatusCommand } from "./status";
@@ -123,7 +123,7 @@ export class DbCommand extends Command {
123
123
  ];
124
124
 
125
125
  // Parent command can have its own execute (optional)
126
- execute(_ctx: AppContext): CommandResult {
126
+ execute(): CommandResult {
127
127
  console.log("Use 'dbctl db <command>' for database operations.");
128
128
  console.log("Available: migrate, seed, status");
129
129
  return { success: true };
@@ -1,4 +1,4 @@
1
- # Guide 5: Interactive TUI (Normal)
1
+ # Guide 5: Interactive TUI
2
2
 
3
3
  Add an auto-generated Terminal User Interface to your CLI.
4
4
 
@@ -8,20 +8,34 @@ A task runner with both CLI and interactive TUI modes:
8
8
 
9
9
  ```bash
10
10
  # CLI mode
11
- taskr run --task build --env production
11
+ taskr --mode cli run --task build --env production
12
12
 
13
- # TUI mode (interactive)
14
- taskr
13
+ # TUI mode
14
+ # (if your app's default mode is a TUI mode)
15
+ taskr
16
+
17
+ # Force TUI mode
18
+ taskr --mode opentui
19
+ # or
20
+ taskr --mode ink
21
+
22
+ # Force CLI mode
23
+ taskr --mode cli
15
24
  ```
16
25
 
17
- When you run without arguments, an interactive form appears!
26
+ Only the selected mode (`--mode`) or your app's default mode controls whether the app runs in CLI or TUI.
18
27
 
19
28
  ## Step 1: Create the Command with TUI Metadata
20
29
 
21
30
  Create `src/commands/run.ts`:
22
31
 
23
32
  ```typescript
24
- import { Command, type AppContext, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
33
+ import {
34
+ Command,
35
+ type OptionSchema,
36
+ type CommandResult,
37
+ type CommandExecutionContext,
38
+ } from "@pablozaiden/terminatui";
25
39
 
26
40
  const options = {
27
41
  task: {
@@ -70,11 +84,9 @@ export class RunCommand extends Command<typeof options, RunConfig> {
70
84
  override readonly displayName = "Run Task";
71
85
  override readonly actionLabel = "Start Task";
72
86
 
73
- async execute(ctx: AppContext, config: RunConfig): Promise<CommandResult> {
74
- ctx.logger.info(`Starting task: ${config.task}`);
75
-
87
+ async execute(config: RunConfig, _execCtx: CommandExecutionContext): Promise<CommandResult> {
76
88
  if (config.verbose) {
77
- ctx.logger.debug(`Environment: ${config.env}`);
89
+ console.debug(`Environment: ${config.env}`);
78
90
  }
79
91
 
80
92
  // Simulate task execution
@@ -82,10 +94,10 @@ export class RunCommand extends Command<typeof options, RunConfig> {
82
94
  await new Promise((resolve) => setTimeout(resolve, 1000));
83
95
  console.log("Task completed!");
84
96
 
85
- return {
86
- success: true,
97
+ return {
98
+ success: true,
87
99
  data: { task: config.task, env: config.env },
88
- message: `Task ${config.task} completed successfully`
100
+ message: `Task ${config.task} completed successfully`,
89
101
  };
90
102
  }
91
103
  }
@@ -100,13 +112,16 @@ import { TuiApplication } from "@pablozaiden/terminatui";
100
112
  import { RunCommand } from "./commands/run";
101
113
 
102
114
  class TaskRunnerApp extends TuiApplication {
115
+ // Default is CLI; each app decides.
116
+ protected override defaultMode = "opentui" as const;
117
+
103
118
  constructor() {
104
119
  super({
105
120
  name: "taskr",
106
- displayName: "🚀 Task Runner", // Shown in TUI header
121
+ displayName: "🚀 Task Runner", // Shown in TUI header
107
122
  version: "1.0.0",
108
123
  commands: [new RunCommand()],
109
- enableTui: true, // Default: true
124
+ enableTui: true, // Default: true
110
125
  });
111
126
  }
112
127
  }
@@ -116,26 +131,26 @@ await new TaskRunnerApp().run();
116
131
 
117
132
  ## Step 3: Test Both Modes
118
133
 
119
- **CLI Mode** (with arguments):
134
+ **CLI Mode** (forced):
120
135
 
121
136
  ```bash
122
- bun src/index.ts run --task build --env production
137
+ bun src/index.ts --mode cli run --task build --env production
123
138
  # Running build in production...
124
139
  # Task completed!
125
140
  ```
126
141
 
127
- **TUI Mode** (no arguments):
142
+ **TUI Mode** (forced):
128
143
 
129
144
  ```bash
130
- bun src/index.ts
145
+ bun src/index.ts --mode opentui
131
146
  ```
132
147
 
133
148
  This opens an interactive interface:
134
149
  - Use ↑/↓ to navigate fields
135
150
  - Press Enter to edit a field
151
+ - Navigate to "CLI Command" button and press Enter to see the CLI command
136
152
  - Press Enter on "Start Task" to run
137
153
  - Press Esc to go back
138
- - Press C to see the CLI command
139
154
 
140
155
  ## TUI Metadata Reference
141
156
 
@@ -145,13 +160,13 @@ Add these properties to your options for TUI customization:
145
160
  {
146
161
  type: "string",
147
162
  description: "...",
148
-
163
+
149
164
  // 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)
165
+ label: "Display Label", // Custom field label
166
+ order: 1, // Field sort order
167
+ group: "Settings", // Group heading
168
+ placeholder: "Enter...", // Placeholder text
169
+ tuiHidden: false, // Hide from TUI (still in CLI)
155
170
  }
156
171
  ```
157
172
 
@@ -161,10 +176,10 @@ Add these properties to your options for TUI customization:
161
176
  class MyCommand extends Command {
162
177
  // Display name in command selector
163
178
  override readonly displayName = "My Command";
164
-
179
+
165
180
  // Button text (default: "Run")
166
181
  override readonly actionLabel = "Execute";
167
-
182
+
168
183
  // Skip config screen, run immediately
169
184
  override readonly immediateExecution = false;
170
185
  }
@@ -175,9 +190,8 @@ class MyCommand extends Command {
175
190
  | Key | Action |
176
191
  |-----|--------|
177
192
  | ↑/↓ | Navigate fields |
178
- | Enter | Edit field / Run |
193
+ | Enter | Edit field / Press button / Run |
179
194
  | Tab | Cycle focus |
180
- | C | Show CLI command |
181
195
  | L | Toggle logs |
182
196
  | Ctrl+Y | Copy to clipboard |
183
197
  | Esc | Back / Cancel |
@@ -185,6 +199,7 @@ class MyCommand extends Command {
185
199
  ## What You Learned
186
200
 
187
201
  - Use `TuiApplication` instead of `Application`
202
+ - Use `--mode` (or app default mode) to control CLI vs TUI
188
203
  - Add TUI metadata to options (label, order, group)
189
204
  - Customize with `displayName` and `actionLabel`
190
205
  - Both CLI and TUI work with the same command
@@ -16,11 +16,9 @@ Create `src/commands/deploy.ts`:
16
16
 
17
17
  ```typescript
18
18
  import path from "node:path";
19
- import { existsSync } from "node:fs";
20
19
  import {
21
20
  Command,
22
21
  ConfigValidationError,
23
- type AppContext,
24
22
  type OptionSchema,
25
23
  type OptionValues,
26
24
  type CommandResult
@@ -84,10 +82,7 @@ export class DeployCommand extends Command<typeof options, DeployConfig> {
84
82
  * Transform and validate raw options into DeployConfig.
85
83
  * Runs before execute() - errors here show helpful messages.
86
84
  */
87
- override buildConfig(
88
- _ctx: AppContext,
89
- opts: OptionValues<typeof options>
90
- ): DeployConfig {
85
+ override async buildConfig(opts: OptionValues<typeof options>): Promise<DeployConfig> {
91
86
  // 1. Validate app path exists
92
87
  const appRaw = opts["app"] as string | undefined;
93
88
  if (!appRaw) {
@@ -98,7 +93,7 @@ export class DeployCommand extends Command<typeof options, DeployConfig> {
98
93
  }
99
94
 
100
95
  const appPath = path.resolve(appRaw);
101
- if (!existsSync(appPath)) {
96
+ if (!(await Bun.file(appPath).exists())) {
102
97
  throw new ConfigValidationError(
103
98
  `Application path does not exist: ${appPath}`,
104
99
  "app"
@@ -157,11 +152,8 @@ export class DeployCommand extends Command<typeof options, DeployConfig> {
157
152
  * Execute with fully validated DeployConfig.
158
153
  * No need to validate here - buildConfig already did it!
159
154
  */
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}`);
155
+ async execute(config: DeployConfig): Promise<CommandResult> {
156
+ console.log(`Deploying ${config.appName} to ${config.environment}`);
165
157
 
166
158
  if (config.dryRun) {
167
159
  console.log("DRY RUN - Would deploy:");