@pablozaiden/terminatui 0.1.2 → 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 +43 -0
  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 +62 -0
  6. package/examples/tui-app/commands/config/app/index.ts +23 -0
  7. package/examples/tui-app/commands/config/app/set.ts +96 -0
  8. package/examples/tui-app/commands/config/index.ts +28 -0
  9. package/examples/tui-app/commands/config/user/get.ts +61 -0
  10. package/examples/tui-app/commands/config/user/index.ts +23 -0
  11. package/examples/tui-app/commands/config/user/set.ts +57 -0
  12. package/examples/tui-app/commands/greet.ts +14 -11
  13. package/examples/tui-app/commands/math.ts +6 -9
  14. package/examples/tui-app/commands/status.ts +24 -13
  15. package/examples/tui-app/index.ts +7 -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 +15 -69
  23. package/guides/08-complete-application.md +13 -179
  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 +19 -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 +52 -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 -3
  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 -582
  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,7 +1,8 @@
1
- import { test, expect, describe, mock, beforeEach, afterEach } from "bun:test";
2
- import { createVersionCommand, formatVersion } from "../builtins/version.ts";
3
- import { createHelpCommand } from "../commands/help.ts";
4
- import { defineCommand } from "../types/command.ts";
1
+ import { test, expect, describe, beforeEach, afterEach } from "bun:test";
2
+ import { createVersionCommand } from "../builtins/version.ts";
3
+ import { createHelpCommandForParent, createRootHelpCommand } from "../builtins/help.ts";
4
+ import { Command } from "../core/command.ts";
5
+ import { KNOWN_COMMANDS } from "../core/knownCommands.ts";
5
6
 
6
7
  describe("Built-in Commands", () => {
7
8
  let originalLog: typeof console.log;
@@ -19,27 +20,6 @@ describe("Built-in Commands", () => {
19
20
  console.log = originalLog;
20
21
  });
21
22
 
22
- describe("formatVersion", () => {
23
- test("formats version with commit hash", () => {
24
- const result = formatVersion("1.0.0", "abc1234567890");
25
- expect(result).toBe("1.0.0 - abc1234");
26
- });
27
-
28
- test("formats version with short commit hash", () => {
29
- const result = formatVersion("1.0.0", "abc1234");
30
- expect(result).toBe("1.0.0 - abc1234");
31
- });
32
-
33
- test("shows (dev) when no commit hash", () => {
34
- const result = formatVersion("1.0.0");
35
- expect(result).toBe("1.0.0 - (dev)");
36
- });
37
-
38
- test("shows (dev) when commit hash is empty", () => {
39
- const result = formatVersion("1.0.0", "");
40
- expect(result).toBe("1.0.0 - (dev)");
41
- });
42
- });
43
23
 
44
24
  describe("VersionCommand", () => {
45
25
  test("creates command with name 'version'", () => {
@@ -70,64 +50,36 @@ describe("Built-in Commands", () => {
70
50
  });
71
51
  });
72
52
 
73
- describe("createHelpCommand", () => {
74
- const mockCommands = [
75
- defineCommand({
76
- name: "run",
77
- description: "Run something",
78
- execute: () => {},
79
- }),
80
- defineCommand({
81
- name: "build",
82
- description: "Build something",
83
- execute: () => {},
84
- }),
85
- ];
86
-
87
- test("creates command with name 'help'", () => {
88
- const cmd = createHelpCommand({ getCommands: () => mockCommands });
89
- expect(cmd.name).toBe("help");
53
+ describe("HelpCommand", () => {
54
+ test("createRootHelpCommand creates command with name 'help'", () => {
55
+ const cmd = createRootHelpCommand([], "myapp", "1.0.0");
56
+ expect(cmd.name).toBe(KNOWN_COMMANDS.help);
90
57
  });
91
58
 
92
- test("has description", () => {
93
- const cmd = createHelpCommand({ getCommands: () => mockCommands });
94
- expect(cmd.description).toBeDefined();
59
+ test("is hidden in TUI", () => {
60
+ const cmd = createRootHelpCommand([], "myapp", "1.0.0");
61
+ expect(cmd.tuiHidden).toBe(true);
62
+ expect(cmd.supportsTui()).toBe(false);
95
63
  });
96
64
 
97
- test("has aliases including --help", () => {
98
- const cmd = createHelpCommand({ getCommands: () => mockCommands });
99
- expect(cmd.aliases).toContain("--help");
65
+ test("execute prints app help", async () => {
66
+ const cmd = createRootHelpCommand([], "myapp", "1.0.0");
67
+ await cmd.execute();
68
+ expect(logOutput.join("\n")).toContain("myapp");
100
69
  });
101
70
 
102
- test("is hidden by default", () => {
103
- const cmd = createHelpCommand({ getCommands: () => mockCommands });
104
- expect(cmd.hidden).toBe(true);
105
- });
71
+ test("createHelpCommandForParent prints parent help", async () => {
72
+ class ParentCommand extends Command {
73
+ readonly name = "run";
74
+ readonly description = "Run something";
75
+ readonly options = {} as const;
106
76
 
107
- test("has command option", () => {
108
- const cmd = createHelpCommand({ getCommands: () => mockCommands });
109
- expect(cmd.options?.["command"]).toBeDefined();
110
- });
111
-
112
- test("execute calls getCommands", () => {
113
- const getCommands = mock(() => mockCommands);
114
- const cmd = createHelpCommand({ getCommands });
115
- // @ts-expect-error - testing with partial options
116
- cmd.execute({ options: {}, args: [], commandPath: ["help"] });
117
- expect(getCommands).toHaveBeenCalled();
118
- });
77
+ override async execute(): Promise<void> {}
78
+ }
119
79
 
120
- test("shows help for specific command when provided", () => {
121
- const cmd = createHelpCommand({
122
- getCommands: () => mockCommands,
123
- appName: "myapp",
124
- });
125
- cmd.execute({
126
- options: { command: "run" },
127
- args: [],
128
- commandPath: ["help"],
129
- });
130
- expect(logOutput.join("")).toContain("run");
80
+ const cmd = createHelpCommandForParent(new ParentCommand(), "myapp", "1.0.0");
81
+ await cmd.execute();
82
+ expect(logOutput.join("\n")).toContain("run");
131
83
  });
132
84
  });
133
85
  });
@@ -1,157 +1,126 @@
1
1
  import { test, expect, describe } from "bun:test";
2
- import { defineCommand } from "../types/command.ts";
3
-
4
- describe("defineCommand", () => {
5
- test("creates a command with name and description", () => {
6
- const cmd = defineCommand({
7
- name: "test",
8
- description: "A test command",
9
- execute: () => {},
10
- });
11
-
2
+ import { Command } from "../core/command.ts";
3
+ import type { OptionSchema, OptionValues } from "../types/command.ts";
4
+
5
+ const testOptions = {
6
+ verbose: {
7
+ type: "boolean",
8
+ description: "Enable verbose output",
9
+ alias: "v",
10
+ },
11
+ name: {
12
+ type: "string",
13
+ description: "Name option",
14
+ },
15
+ } as const satisfies OptionSchema;
16
+
17
+ describe("Command (class-based)", () => {
18
+ test("has name and description", () => {
19
+ class TestCommand extends Command<typeof testOptions> {
20
+ readonly name = "test";
21
+ readonly description = "A test command";
22
+ readonly options = testOptions;
23
+
24
+ override async execute(): Promise<void> {}
25
+ }
26
+
27
+ const cmd = new TestCommand();
12
28
  expect(cmd.name).toBe("test");
13
29
  expect(cmd.description).toBe("A test command");
14
30
  });
15
31
 
16
- test("creates a command with options", () => {
17
- const cmd = defineCommand({
18
- name: "test",
19
- description: "A test command",
20
- options: {
21
- verbose: {
22
- type: "boolean",
23
- description: "Enable verbose output",
24
- alias: "v",
25
- },
26
- },
27
- execute: () => {},
28
- });
29
-
30
- expect(cmd.options?.["verbose"]).toBeDefined();
31
- expect(cmd.options?.["verbose"]?.type).toBe("boolean");
32
- });
32
+ test("has options", () => {
33
+ class TestCommand extends Command<typeof testOptions> {
34
+ readonly name = "test";
35
+ readonly description = "A test command";
36
+ readonly options = testOptions;
33
37
 
34
- test("creates a command with aliases", () => {
35
- const cmd = defineCommand({
36
- name: "test",
37
- description: "A test command",
38
- aliases: ["t", "tst"],
39
- execute: () => {},
40
- });
38
+ override async execute(): Promise<void> {}
39
+ }
41
40
 
42
- expect(cmd.aliases).toEqual(["t", "tst"]);
41
+ const cmd = new TestCommand();
42
+ expect(cmd.options["verbose"]?.type).toBe("boolean");
43
43
  });
44
44
 
45
- test("creates a command with subcommands", () => {
46
- const sub = defineCommand({
47
- name: "sub",
48
- description: "A subcommand",
49
- execute: () => {},
50
- });
51
-
52
- const cmd = defineCommand({
53
- name: "parent",
54
- description: "A parent command",
55
- subcommands: { sub },
56
- execute: () => {},
57
- });
58
-
59
- expect(cmd.subcommands?.["sub"]).toBe(sub);
60
- });
45
+ test("supports subcommands", () => {
46
+ class SubCommand extends Command<OptionSchema> {
47
+ readonly name = "sub";
48
+ readonly description = "A subcommand";
49
+ readonly options = {} as const;
61
50
 
62
- test("executes sync command", () => {
63
- let executed = false;
64
- const cmd = defineCommand({
65
- name: "test",
66
- description: "A test command",
67
- execute: () => {
68
- executed = true;
69
- },
70
- });
71
-
72
- cmd.execute({ options: {}, args: [], commandPath: ["test"] });
73
- expect(executed).toBe(true);
74
- });
51
+ override async execute(): Promise<void> {}
52
+ }
75
53
 
76
- test("executes async command", async () => {
77
- let executed = false;
78
- const cmd = defineCommand({
79
- name: "test",
80
- description: "A test command",
81
- execute: async () => {
82
- await new Promise((r) => setTimeout(r, 10));
83
- executed = true;
84
- },
85
- });
86
-
87
- await cmd.execute({ options: {}, args: [], commandPath: ["test"] });
88
- expect(executed).toBe(true);
89
- });
54
+ class ParentCommand extends Command<OptionSchema> {
55
+ readonly name = "parent";
56
+ readonly description = "A parent command";
57
+ readonly options = {} as const;
58
+ override subCommands = [new SubCommand()];
59
+
60
+ override async execute(): Promise<void> {}
61
+ }
90
62
 
91
- test("passes options to execute", () => {
92
- let receivedOptions: unknown;
93
- const cmd = defineCommand({
94
- name: "test",
95
- description: "A test command",
96
- options: {
97
- name: {
98
- type: "string",
99
- description: "Name option",
100
- },
101
- },
102
- execute: (ctx) => {
103
- receivedOptions = ctx.options;
104
- },
105
- });
106
-
107
- cmd.execute({ options: { name: "world" }, args: [], commandPath: ["test"] });
108
- expect(receivedOptions).toEqual({ name: "world" });
63
+ const cmd = new ParentCommand();
64
+ expect(cmd.getSubCommand("sub")?.name).toBe("sub");
109
65
  });
110
66
 
111
- test("supports beforeExecute hook", async () => {
112
- const order: string[] = [];
113
- const cmd = defineCommand({
114
- name: "test",
115
- description: "A test command",
116
- beforeExecute: () => {
117
- order.push("before");
118
- },
119
- execute: () => {
120
- order.push("execute");
121
- },
122
- });
67
+ test("executes command", async () => {
68
+ class ExecCommand extends Command<typeof testOptions> {
69
+ readonly name = "exec";
70
+ readonly description = "Exec command";
71
+ readonly options = testOptions;
123
72
 
124
- await cmd.beforeExecute?.({ options: {}, args: [], commandPath: ["test"] });
125
- await cmd.execute({ options: {}, args: [], commandPath: ["test"] });
126
- expect(order).toEqual(["before", "execute"]);
73
+ executedWith: OptionValues<typeof testOptions> | null = null;
74
+
75
+ override async execute(opts: OptionValues<typeof testOptions>): Promise<void> {
76
+ this.executedWith = opts;
77
+ }
78
+ }
79
+
80
+ const cmd = new ExecCommand();
81
+ await cmd.execute({ verbose: true, name: "world" });
82
+ expect(cmd.executedWith).toEqual({ verbose: true, name: "world" });
127
83
  });
128
84
 
129
- test("supports afterExecute hook", async () => {
85
+ test("beforeExecute/afterExecute hooks are callable", () => {
130
86
  const order: string[] = [];
131
- const cmd = defineCommand({
132
- name: "test",
133
- description: "A test command",
134
- execute: () => {
87
+
88
+ class HookCommand extends Command<OptionSchema> {
89
+ readonly name = "hook";
90
+ readonly description = "Hook command";
91
+ readonly options = {} as const;
92
+
93
+ override beforeExecute(): void {
94
+ order.push("before");
95
+ }
96
+
97
+ override execute(): void {
135
98
  order.push("execute");
136
- },
137
- afterExecute: () => {
99
+ }
100
+
101
+ override afterExecute(): void {
138
102
  order.push("after");
139
- },
140
- });
103
+ }
104
+ }
141
105
 
142
- await cmd.execute({ options: {}, args: [], commandPath: ["test"] });
143
- await cmd.afterExecute?.({ options: {}, args: [], commandPath: ["test"] });
144
- expect(order).toEqual(["execute", "after"]);
106
+ const cmd = new HookCommand();
107
+ cmd.beforeExecute();
108
+ cmd.execute();
109
+ cmd.afterExecute();
110
+
111
+ expect(order).toEqual(["before", "execute", "after"]);
145
112
  });
146
113
 
147
- test("supports hidden commands", () => {
148
- const cmd = defineCommand({
149
- name: "hidden",
150
- description: "A hidden command",
151
- hidden: true,
152
- execute: () => {},
153
- });
114
+ test("supports tuiHidden", () => {
115
+ class HiddenCommand extends Command<OptionSchema> {
116
+ readonly name = "hidden";
117
+ readonly description = "Hidden";
118
+ readonly options = {} as const;
119
+ override readonly tuiHidden = true;
120
+
121
+ override async execute(): Promise<void> {}
122
+ }
154
123
 
155
- expect(cmd.hidden).toBe(true);
124
+ expect(new HiddenCommand().tuiHidden).toBe(true);
156
125
  });
157
126
  });
@@ -3,7 +3,7 @@ import { AppContext, type AppConfig } from "../core/context.ts";
3
3
 
4
4
  describe("AppContext", () => {
5
5
  afterEach(() => {
6
- AppContext.clearCurrent();
6
+ AppContext.setCurrent(new AppContext({ name: "empty", version: "0.0.0" }));
7
7
  });
8
8
 
9
9
  describe("constructor", () => {
@@ -22,37 +22,12 @@ describe("AppContext", () => {
22
22
  });
23
23
 
24
24
  describe("static current", () => {
25
- test("throws when no current context", () => {
26
- expect(() => AppContext.current).toThrow(
27
- "AppContext.current accessed before initialization"
28
- );
29
- });
30
-
31
25
  test("returns current context after setCurrent", () => {
32
26
  const config: AppConfig = { name: "test", version: "1.0.0" };
33
27
  const ctx = new AppContext(config);
34
28
  AppContext.setCurrent(ctx);
35
29
  expect(AppContext.current).toBe(ctx);
36
30
  });
37
-
38
- test("hasCurrent returns false when no context", () => {
39
- expect(AppContext.hasCurrent()).toBe(false);
40
- });
41
-
42
- test("hasCurrent returns true after setCurrent", () => {
43
- const config: AppConfig = { name: "test", version: "1.0.0" };
44
- const ctx = new AppContext(config);
45
- AppContext.setCurrent(ctx);
46
- expect(AppContext.hasCurrent()).toBe(true);
47
- });
48
-
49
- test("clearCurrent clears the context", () => {
50
- const config: AppConfig = { name: "test", version: "1.0.0" };
51
- const ctx = new AppContext(config);
52
- AppContext.setCurrent(ctx);
53
- AppContext.clearCurrent();
54
- expect(AppContext.hasCurrent()).toBe(false);
55
- });
56
31
  });
57
32
 
58
33
  describe("services", () => {
@@ -0,0 +1,227 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ formatExamples,
4
+ formatOptionSchema,
5
+ formatOptions,
6
+ formatSubCommands,
7
+ formatUsage,
8
+ generateAppHelp,
9
+ generateCommandHelp,
10
+ } from "../core/help.ts";
11
+ import { Command } from "../core/command.ts";
12
+ import type { OptionSchema } from "../types/command.ts";
13
+
14
+ class SimpleCommand extends Command<OptionSchema> {
15
+ readonly name: string;
16
+ readonly description: string;
17
+ readonly options: OptionSchema;
18
+
19
+ constructor(config: { name: string; description: string; options?: OptionSchema }) {
20
+ super();
21
+ this.name = config.name;
22
+ this.description = config.description;
23
+ this.options = config.options ?? {};
24
+ }
25
+
26
+ override async execute(): Promise<void> {}
27
+ }
28
+
29
+ describe("Help Generation (core)", () => {
30
+ describe("formatUsage", () => {
31
+ test("formats basic usage and includes tokens", () => {
32
+ const cmd = new SimpleCommand({
33
+ name: "parent",
34
+ description: "Parent",
35
+ options: { verbose: { type: "boolean", description: "Verbose" } },
36
+ });
37
+ cmd.subCommands = [new SimpleCommand({ name: "child", description: "Child" })];
38
+
39
+ const usage = formatUsage(cmd, { appName: "myapp" });
40
+ expect(usage).toContain("myapp");
41
+ expect(usage).toContain("parent");
42
+ expect(usage).toContain("[command]");
43
+ expect(usage).toContain("[options]");
44
+ });
45
+ });
46
+
47
+ describe("formatSubCommands", () => {
48
+ test("formats subcommands list", () => {
49
+ const cmd = new SimpleCommand({ name: "parent", description: "Parent" });
50
+ cmd.subCommands = [new SimpleCommand({ name: "child", description: "Child command" })];
51
+
52
+ const commands = formatSubCommands(cmd);
53
+ expect(commands).toContain("child");
54
+ expect(commands).toContain("Child command");
55
+ });
56
+
57
+ test("returns empty for no subcommands", () => {
58
+ const cmd = new SimpleCommand({ name: "test", description: "Test" });
59
+ const commands = formatSubCommands(cmd);
60
+ expect(commands).toBe("");
61
+ });
62
+ });
63
+
64
+ describe("formatOptions", () => {
65
+ test("formats options with descriptions", () => {
66
+ const cmd = new SimpleCommand({
67
+ name: "test",
68
+ description: "Test",
69
+ options: { verbose: { type: "boolean", description: "Enable verbose" } },
70
+ });
71
+
72
+ const options = formatOptions(cmd);
73
+ expect(options).toContain("--verbose");
74
+ expect(options).toContain("Enable verbose");
75
+ });
76
+
77
+ test("shows option aliases", () => {
78
+ const cmd = new SimpleCommand({
79
+ name: "test",
80
+ description: "Test",
81
+ options: { verbose: { type: "boolean", alias: "v", description: "Verbose" } },
82
+ });
83
+
84
+ const options = formatOptions(cmd);
85
+ expect(options).toContain("-v");
86
+ });
87
+
88
+ test("shows default values", () => {
89
+ const cmd = new SimpleCommand({
90
+ name: "test",
91
+ description: "Test",
92
+ options: { count: { type: "number", default: 10, description: "Count" } },
93
+ });
94
+
95
+ const options = formatOptions(cmd);
96
+ expect(options).toContain("10");
97
+ });
98
+
99
+ test("shows required marker", () => {
100
+ const cmd = new SimpleCommand({
101
+ name: "test",
102
+ description: "Test",
103
+ options: { name: { type: "string", required: true, description: "Name" } },
104
+ });
105
+
106
+ const options = formatOptions(cmd);
107
+ expect(options).toContain("required");
108
+ });
109
+
110
+ test("shows enum values", () => {
111
+ const cmd = new SimpleCommand({
112
+ name: "test",
113
+ description: "Test",
114
+ options: {
115
+ level: {
116
+ type: "string",
117
+ enum: ["low", "high"],
118
+ description: "Level",
119
+ },
120
+ },
121
+ });
122
+
123
+ const options = formatOptions(cmd);
124
+ expect(options).toContain("low");
125
+ expect(options).toContain("high");
126
+ });
127
+
128
+ test("returns empty for no options", () => {
129
+ const cmd = new SimpleCommand({ name: "test", description: "Test" });
130
+ const options = formatOptions(cmd);
131
+ expect(options).toBe("");
132
+ });
133
+ });
134
+
135
+ describe("formatExamples", () => {
136
+ test("formats examples list and empty state", () => {
137
+ const cmd = new SimpleCommand({ name: "test", description: "Test" });
138
+ cmd.examples = [{ command: "test --verbose", description: "Run with verbose" }];
139
+
140
+ expect(formatExamples(cmd)).toContain("test --verbose");
141
+ expect(formatExamples(cmd)).toContain("Run with verbose");
142
+
143
+ const noExamples = new SimpleCommand({ name: "empty", description: "Empty" });
144
+ expect(formatExamples(noExamples)).toBe("");
145
+ });
146
+ });
147
+
148
+ describe("generateCommandHelp", () => {
149
+ test("includes usage, description, and options section", () => {
150
+ const cmd = new SimpleCommand({
151
+ name: "test",
152
+ description: "A test command for testing",
153
+ options: { verbose: { type: "boolean", description: "Verbose mode" } },
154
+ });
155
+
156
+ const help = generateCommandHelp(cmd, { appName: "myapp" });
157
+ expect(help).toContain("Usage:");
158
+ expect(help).toContain("A test command for testing");
159
+ expect(help).toContain("Options:");
160
+ expect(help).toContain("--verbose");
161
+ expect(help).toContain("--verbose, --no-verbose");
162
+ });
163
+ });
164
+
165
+ describe("generateAppHelp", () => {
166
+ test("generates root help with commands", () => {
167
+ const commands = [new SimpleCommand({ name: "run", description: "Run something" })];
168
+ const help = generateAppHelp(commands, { appName: "myapp" });
169
+ expect(help).toContain("Commands:");
170
+ expect(help).toContain("run");
171
+ });
172
+ });
173
+
174
+ describe("Global Options section", () => {
175
+ test("modes via schema and includes mode", () => {
176
+ const global = formatOptionSchema("Global Options", {
177
+ "log-level": { type: "string", description: "Minimum log level" },
178
+ mode: {
179
+ type: "string",
180
+ enum: ["opentui", "ink", "cli", "default"],
181
+ default: "default",
182
+ description: "Mode",
183
+ },
184
+ });
185
+
186
+ expect(global).toContain("Global Options");
187
+ expect(global).toContain("--log-level");
188
+ expect(global).toContain("--mode");
189
+ expect(global).toContain("opentui");
190
+ expect(global).toContain("ink");
191
+ expect(global).toContain("cli");
192
+ expect(global).toContain("default");
193
+ expect(global).toContain("[default: default]");
194
+ });
195
+
196
+ test("generateCommandHelp includes global options when provided", () => {
197
+ const cmd = new SimpleCommand({ name: "test", description: "Test" });
198
+ const help = generateCommandHelp(cmd, {
199
+ appName: "myapp",
200
+ globalOptionsSchema: {
201
+ mode: {
202
+ type: "string",
203
+ enum: ["opentui", "ink", "cli", "default"],
204
+ default: "default",
205
+ description: "Mode",
206
+ },
207
+ },
208
+ });
209
+
210
+ expect(help).toContain("Global Options");
211
+ expect(help).toContain("--mode");
212
+ });
213
+
214
+ test("generateAppHelp includes global options when provided", () => {
215
+ const commands = [new SimpleCommand({ name: "run", description: "Run something" })];
216
+ const help = generateAppHelp(commands, {
217
+ appName: "myapp",
218
+ globalOptionsSchema: {
219
+ mode: { type: "string", enum: ["opentui", "ink", "cli", "default"], description: "Mode" },
220
+ },
221
+ });
222
+
223
+ expect(help).toContain("Global Options");
224
+ expect(help).toContain("--mode");
225
+ });
226
+ });
227
+ });