@pablozaiden/terminatui 0.2.0 → 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 (181) hide show
  1. package/README.md +64 -43
  2. package/package.json +11 -8
  3. package/src/__tests__/application.test.ts +87 -68
  4. package/src/__tests__/buildCliCommand.test.ts +99 -119
  5. package/src/__tests__/builtins.test.ts +27 -75
  6. package/src/__tests__/command.test.ts +100 -131
  7. package/src/__tests__/configOnChange.test.ts +63 -0
  8. package/src/__tests__/context.test.ts +1 -26
  9. package/src/__tests__/helpCore.test.ts +227 -0
  10. package/src/__tests__/parser.test.ts +98 -244
  11. package/src/__tests__/registry.test.ts +33 -160
  12. package/src/__tests__/schemaToFields.test.ts +75 -158
  13. package/src/builtins/help.ts +12 -4
  14. package/src/builtins/settings.ts +18 -32
  15. package/src/builtins/version.ts +3 -3
  16. package/src/cli/output/colors.ts +1 -1
  17. package/src/cli/parser.ts +26 -95
  18. package/src/core/application.ts +192 -110
  19. package/src/core/command.ts +26 -9
  20. package/src/core/context.ts +31 -20
  21. package/src/core/help.ts +24 -18
  22. package/src/core/knownCommands.ts +13 -0
  23. package/src/core/logger.ts +39 -42
  24. package/src/core/registry.ts +5 -12
  25. package/src/index.ts +22 -137
  26. package/src/tui/TuiApplication.tsx +63 -120
  27. package/src/tui/TuiRoot.tsx +135 -0
  28. package/src/tui/adapters/factory.ts +19 -0
  29. package/src/tui/adapters/ink/InkRenderer.tsx +139 -0
  30. package/src/tui/adapters/ink/components/Button.tsx +12 -0
  31. package/src/tui/adapters/ink/components/Code.tsx +6 -0
  32. package/src/tui/adapters/ink/components/CodeHighlight.tsx +6 -0
  33. package/src/tui/adapters/ink/components/Container.tsx +5 -0
  34. package/src/tui/adapters/ink/components/Field.tsx +12 -0
  35. package/src/tui/adapters/ink/components/Label.tsx +24 -0
  36. package/src/tui/adapters/ink/components/MenuButton.tsx +12 -0
  37. package/src/tui/adapters/ink/components/MenuItem.tsx +17 -0
  38. package/src/tui/adapters/ink/components/Overlay.tsx +5 -0
  39. package/src/tui/adapters/ink/components/Panel.tsx +15 -0
  40. package/src/tui/adapters/ink/components/ScrollView.tsx +5 -0
  41. package/src/tui/adapters/ink/components/Select.tsx +44 -0
  42. package/src/tui/adapters/ink/components/Spacer.tsx +15 -0
  43. package/src/tui/adapters/ink/components/Spinner.tsx +5 -0
  44. package/src/tui/adapters/ink/components/TextInput.tsx +22 -0
  45. package/src/tui/adapters/ink/components/Value.tsx +7 -0
  46. package/src/tui/adapters/ink/keyboard.ts +97 -0
  47. package/src/tui/adapters/ink/utils.ts +16 -0
  48. package/src/tui/adapters/opentui/OpenTuiRenderer.tsx +119 -0
  49. package/src/tui/adapters/opentui/components/Button.tsx +13 -0
  50. package/src/tui/adapters/opentui/components/Code.tsx +12 -0
  51. package/src/tui/adapters/opentui/components/CodeHighlight.tsx +24 -0
  52. package/src/tui/adapters/opentui/components/Container.tsx +56 -0
  53. package/src/tui/adapters/opentui/components/Field.tsx +18 -0
  54. package/src/tui/adapters/opentui/components/Label.tsx +15 -0
  55. package/src/tui/adapters/opentui/components/MenuButton.tsx +14 -0
  56. package/src/tui/adapters/opentui/components/MenuItem.tsx +29 -0
  57. package/src/tui/adapters/opentui/components/Overlay.tsx +21 -0
  58. package/src/tui/adapters/opentui/components/Panel.tsx +78 -0
  59. package/src/tui/adapters/opentui/components/ScrollView.tsx +85 -0
  60. package/src/tui/adapters/opentui/components/Select.tsx +59 -0
  61. package/src/tui/adapters/opentui/components/Spacer.tsx +5 -0
  62. package/src/tui/adapters/opentui/components/Spinner.tsx +12 -0
  63. package/src/tui/adapters/opentui/components/TextInput.tsx +13 -0
  64. package/src/tui/adapters/opentui/components/Value.tsx +13 -0
  65. package/src/tui/{hooks → adapters/opentui/hooks}/useSpinner.ts +2 -11
  66. package/src/tui/adapters/opentui/keyboard.ts +61 -0
  67. package/src/tui/adapters/types.ts +71 -0
  68. package/src/tui/components/ActionButton.tsx +0 -36
  69. package/src/tui/components/CommandSelector.tsx +45 -92
  70. package/src/tui/components/ConfigForm.tsx +68 -42
  71. package/src/tui/components/FieldRow.tsx +0 -30
  72. package/src/tui/components/Header.tsx +14 -13
  73. package/src/tui/components/JsonHighlight.tsx +10 -17
  74. package/src/tui/components/ModalBase.tsx +38 -0
  75. package/src/tui/components/ResultsPanel.tsx +27 -36
  76. package/src/tui/components/StatusBar.tsx +24 -39
  77. package/src/tui/components/logColors.ts +12 -0
  78. package/src/tui/context/ClipboardContext.tsx +87 -0
  79. package/src/tui/context/ExecutorContext.tsx +139 -0
  80. package/src/tui/context/KeyboardContext.tsx +85 -71
  81. package/src/tui/context/LogsContext.tsx +35 -0
  82. package/src/tui/context/NavigationContext.tsx +194 -0
  83. package/src/tui/context/RendererContext.tsx +20 -0
  84. package/src/tui/context/TuiAppContext.tsx +58 -0
  85. package/src/tui/hooks/useActiveKeyHandler.ts +75 -0
  86. package/src/tui/hooks/useBackHandler.ts +34 -0
  87. package/src/tui/hooks/useClipboard.ts +40 -25
  88. package/src/tui/hooks/useClipboardProvider.ts +42 -0
  89. package/src/tui/hooks/useGlobalKeyHandler.ts +54 -0
  90. package/src/tui/modals/CliModal.tsx +82 -0
  91. package/src/tui/modals/EditorModal.tsx +207 -0
  92. package/src/tui/modals/LogsModal.tsx +98 -0
  93. package/src/tui/registry.ts +102 -0
  94. package/src/tui/screens/CommandSelectScreen.tsx +162 -0
  95. package/src/tui/screens/ConfigScreen.tsx +165 -0
  96. package/src/tui/screens/ErrorScreen.tsx +58 -0
  97. package/src/tui/screens/ResultsScreen.tsx +68 -0
  98. package/src/tui/screens/RunningScreen.tsx +72 -0
  99. package/src/tui/screens/ScreenBase.ts +6 -0
  100. package/src/tui/semantic/Button.tsx +7 -0
  101. package/src/tui/semantic/Code.tsx +7 -0
  102. package/src/tui/semantic/CodeHighlight.tsx +7 -0
  103. package/src/tui/semantic/Container.tsx +7 -0
  104. package/src/tui/semantic/Field.tsx +7 -0
  105. package/src/tui/semantic/Label.tsx +7 -0
  106. package/src/tui/semantic/MenuButton.tsx +7 -0
  107. package/src/tui/semantic/MenuItem.tsx +7 -0
  108. package/src/tui/semantic/Overlay.tsx +7 -0
  109. package/src/tui/semantic/Panel.tsx +7 -0
  110. package/src/tui/semantic/ScrollView.tsx +9 -0
  111. package/src/tui/semantic/Select.tsx +7 -0
  112. package/src/tui/semantic/Spacer.tsx +7 -0
  113. package/src/tui/semantic/Spinner.tsx +7 -0
  114. package/src/tui/semantic/TextInput.tsx +7 -0
  115. package/src/tui/semantic/Value.tsx +7 -0
  116. package/src/tui/semantic/types.ts +195 -0
  117. package/src/tui/theme.ts +25 -14
  118. package/src/tui/utils/buildCliCommand.ts +1 -0
  119. package/src/tui/utils/getEnumKeys.ts +3 -0
  120. package/src/tui/utils/parameterPersistence.ts +1 -0
  121. package/src/types/command.ts +0 -60
  122. package/.devcontainer/devcontainer.json +0 -19
  123. package/.devcontainer/install-prerequisites.sh +0 -49
  124. package/.github/workflows/copilot-setup-steps.yml +0 -32
  125. package/.github/workflows/pull-request.yml +0 -27
  126. package/.github/workflows/release-npm-package.yml +0 -81
  127. package/AGENTS.md +0 -31
  128. package/bun.lock +0 -236
  129. package/examples/tui-app/commands/config/app/get.ts +0 -66
  130. package/examples/tui-app/commands/config/app/index.ts +0 -27
  131. package/examples/tui-app/commands/config/app/set.ts +0 -86
  132. package/examples/tui-app/commands/config/index.ts +0 -32
  133. package/examples/tui-app/commands/config/user/get.ts +0 -65
  134. package/examples/tui-app/commands/config/user/index.ts +0 -27
  135. package/examples/tui-app/commands/config/user/set.ts +0 -61
  136. package/examples/tui-app/commands/greet.ts +0 -76
  137. package/examples/tui-app/commands/index.ts +0 -4
  138. package/examples/tui-app/commands/math.ts +0 -115
  139. package/examples/tui-app/commands/status.ts +0 -77
  140. package/examples/tui-app/index.ts +0 -35
  141. package/guides/01-hello-world.md +0 -96
  142. package/guides/02-adding-options.md +0 -103
  143. package/guides/03-multiple-commands.md +0 -163
  144. package/guides/04-subcommands.md +0 -206
  145. package/guides/05-interactive-tui.md +0 -194
  146. package/guides/06-config-validation.md +0 -264
  147. package/guides/07-async-cancellation.md +0 -336
  148. package/guides/08-complete-application.md +0 -537
  149. package/guides/README.md +0 -74
  150. package/src/__tests__/colors.test.ts +0 -127
  151. package/src/__tests__/commandClass.test.ts +0 -130
  152. package/src/__tests__/help.test.ts +0 -412
  153. package/src/__tests__/registryNew.test.ts +0 -160
  154. package/src/__tests__/table.test.ts +0 -146
  155. package/src/__tests__/tui.test.ts +0 -26
  156. package/src/builtins/index.ts +0 -4
  157. package/src/cli/help.ts +0 -174
  158. package/src/cli/index.ts +0 -3
  159. package/src/cli/output/index.ts +0 -2
  160. package/src/cli/output/table.ts +0 -141
  161. package/src/commands/help.ts +0 -50
  162. package/src/commands/index.ts +0 -1
  163. package/src/components/index.ts +0 -147
  164. package/src/core/index.ts +0 -15
  165. package/src/hooks/index.ts +0 -131
  166. package/src/registry/commandRegistry.ts +0 -77
  167. package/src/registry/index.ts +0 -1
  168. package/src/tui/TuiApp.tsx +0 -619
  169. package/src/tui/app.ts +0 -29
  170. package/src/tui/components/CliModal.tsx +0 -81
  171. package/src/tui/components/EditorModal.tsx +0 -177
  172. package/src/tui/components/LogsPanel.tsx +0 -86
  173. package/src/tui/components/index.ts +0 -13
  174. package/src/tui/context/index.ts +0 -7
  175. package/src/tui/hooks/index.ts +0 -35
  176. package/src/tui/hooks/useKeyboardHandler.ts +0 -91
  177. package/src/tui/hooks/useLogStream.ts +0 -96
  178. package/src/tui/index.ts +0 -65
  179. package/src/tui/utils/index.ts +0 -13
  180. package/src/types/index.ts +0 -1
  181. package/tsconfig.json +0 -25
@@ -1,66 +1,45 @@
1
1
  import { test, expect, describe } from "bun:test";
2
- import { createCommandRegistry } from "../registry/commandRegistry.ts";
3
- import { defineCommand } from "../types/command.ts";
2
+ import { CommandRegistry } from "../core/registry.ts";
3
+ import { Command } from "../core/command.ts";
4
+ import type { OptionSchema } from "../types/command.ts";
4
5
 
5
- describe("createCommandRegistry", () => {
6
- test("registers a command", () => {
7
- const registry = createCommandRegistry();
8
- const cmd = defineCommand({
9
- name: "test",
10
- description: "Test command",
11
- execute: () => {},
12
- });
6
+ class SimpleCommand extends Command<OptionSchema> {
7
+ readonly name: string;
8
+ readonly description: string;
9
+ readonly options = {} as const;
13
10
 
14
- registry.register(cmd);
11
+ constructor(name: string, description = "") {
12
+ super();
13
+ this.name = name;
14
+ this.description = description;
15
+ }
16
+
17
+ override async execute(): Promise<void> {}
18
+ }
15
19
 
20
+ describe("CommandRegistry", () => {
21
+ test("registers a command", () => {
22
+ const registry = new CommandRegistry();
23
+ registry.register(new SimpleCommand("test", "Test command"));
16
24
  expect(registry.has("test")).toBe(true);
17
25
  });
18
26
 
19
27
  test("retrieves command by name", () => {
20
- const registry = createCommandRegistry();
21
- const cmd = defineCommand({
22
- name: "greet",
23
- description: "Greet command",
24
- execute: () => {},
25
- });
26
-
28
+ const registry = new CommandRegistry();
29
+ const cmd = new SimpleCommand("greet", "Greet command");
27
30
  registry.register(cmd);
28
-
29
31
  expect(registry.get("greet")).toBe(cmd);
30
32
  });
31
33
 
32
- test("resolves command by alias", () => {
33
- const registry = createCommandRegistry();
34
- const cmd = defineCommand({
35
- name: "list",
36
- description: "List command",
37
- aliases: ["ls", "l"],
38
- execute: () => {},
39
- });
40
-
41
- registry.register(cmd);
42
-
43
- expect(registry.resolve("ls")).toBe(cmd);
44
- expect(registry.resolve("l")).toBe(cmd);
45
- });
46
-
47
34
  test("returns undefined for unknown command", () => {
48
- const registry = createCommandRegistry();
35
+ const registry = new CommandRegistry();
49
36
  expect(registry.get("unknown")).toBeUndefined();
50
37
  });
51
38
 
52
39
  test("lists all commands", () => {
53
- const registry = createCommandRegistry();
54
- const cmd1 = defineCommand({
55
- name: "a",
56
- description: "A",
57
- execute: () => {},
58
- });
59
- const cmd2 = defineCommand({
60
- name: "b",
61
- description: "B",
62
- execute: () => {},
63
- });
40
+ const registry = new CommandRegistry();
41
+ const cmd1 = new SimpleCommand("a", "A");
42
+ const cmd2 = new SimpleCommand("b", "B");
64
43
 
65
44
  registry.register(cmd1);
66
45
  registry.register(cmd2);
@@ -71,125 +50,19 @@ describe("createCommandRegistry", () => {
71
50
  });
72
51
 
73
52
  test("throws on duplicate registration", () => {
74
- const registry = createCommandRegistry();
75
- const cmd = defineCommand({
76
- name: "dup",
77
- description: "Duplicate",
78
- execute: () => {},
79
- });
80
-
81
- registry.register(cmd);
82
-
83
- expect(() => registry.register(cmd)).toThrow();
84
- });
85
-
86
- test("throws on alias conflict", () => {
87
- const registry = createCommandRegistry();
88
- const cmd1 = defineCommand({
89
- name: "first",
90
- description: "First",
91
- aliases: ["f"],
92
- execute: () => {},
93
- });
94
- const cmd2 = defineCommand({
95
- name: "second",
96
- description: "Second",
97
- aliases: ["f"],
98
- execute: () => {},
99
- });
100
-
101
- registry.register(cmd1);
102
-
103
- expect(() => registry.register(cmd2)).toThrow();
104
- });
105
-
106
- test("has method checks existence by name", () => {
107
- const registry = createCommandRegistry();
108
- const cmd = defineCommand({
109
- name: "exists",
110
- description: "Exists",
111
- execute: () => {},
112
- });
113
-
114
- registry.register(cmd);
115
-
116
- expect(registry.has("exists")).toBe(true);
117
- expect(registry.has("notexists")).toBe(false);
118
- });
119
-
120
- test("has method checks existence by alias", () => {
121
- const registry = createCommandRegistry();
122
- const cmd = defineCommand({
123
- name: "cmd",
124
- description: "Cmd",
125
- aliases: ["c"],
126
- execute: () => {},
127
- });
128
-
53
+ const registry = new CommandRegistry();
54
+ const cmd = new SimpleCommand("dup", "Duplicate");
129
55
  registry.register(cmd);
130
-
131
- expect(registry.has("c")).toBe(true);
56
+ expect(() => registry.register(cmd)).toThrow(/already registered/i);
132
57
  });
133
58
 
134
- test("getNames returns command names", () => {
135
- const registry = createCommandRegistry();
136
- const cmdA = defineCommand({
137
- name: "a",
138
- description: "A",
139
- execute: () => {},
140
- });
141
- const cmdB = defineCommand({
142
- name: "b",
143
- description: "B",
144
- execute: () => {},
145
- });
59
+ test("names returns command names", () => {
60
+ const registry = new CommandRegistry();
61
+ registry.register(new SimpleCommand("a", "A"));
62
+ registry.register(new SimpleCommand("b", "B"));
146
63
 
147
- registry.register(cmdA);
148
- registry.register(cmdB);
149
-
150
- const names = registry.getNames();
64
+ const names = registry.names();
151
65
  expect(names).toContain("a");
152
66
  expect(names).toContain("b");
153
67
  });
154
-
155
- test("getCommandMap returns command map", () => {
156
- const registry = createCommandRegistry();
157
- const cmdA = defineCommand({
158
- name: "a",
159
- description: "A",
160
- execute: () => {},
161
- });
162
- const cmdB = defineCommand({
163
- name: "b",
164
- description: "B",
165
- execute: () => {},
166
- });
167
-
168
- registry.register(cmdA);
169
- registry.register(cmdB);
170
-
171
- const map = registry.getCommandMap();
172
- expect(map["a"]).toBe(cmdA);
173
- expect(map["b"]).toBe(cmdB);
174
- });
175
-
176
- test("resolve returns undefined for non-existent command", () => {
177
- const registry = createCommandRegistry();
178
- expect(registry.resolve("nonexistent")).toBeUndefined();
179
- });
180
-
181
- test("get does not resolve aliases", () => {
182
- const registry = createCommandRegistry();
183
- const cmd = defineCommand({
184
- name: "list",
185
- description: "List command",
186
- aliases: ["ls"],
187
- execute: () => {},
188
- });
189
-
190
- registry.register(cmd);
191
-
192
- expect(registry.get("ls")).toBeUndefined();
193
- expect(registry.resolve("ls")).toBe(cmd);
194
- });
195
68
  });
@@ -1,176 +1,93 @@
1
- import { describe, test, expect } from "bun:test";
2
- import { schemaToFieldConfigs, getFieldDisplayValue } from "../tui/utils/schemaToFields.ts";
1
+ import { describe, expect, test } from "bun:test";
2
+ import { getFieldDisplayValue, schemaToFieldConfigs } from "../tui/utils/schemaToFields.ts";
3
3
  import type { OptionSchema } from "../types/command.ts";
4
4
 
5
- describe("schemaToFieldConfigs", () => {
6
- test("converts string type to text field", () => {
7
- const schema: OptionSchema = {
8
- name: {
9
- type: "string",
10
- description: "User name",
11
- },
12
- };
13
-
14
- const fields = schemaToFieldConfigs(schema);
15
- expect(fields).toHaveLength(1);
16
- expect(fields[0]?.key).toBe("name");
17
- expect(fields[0]?.type).toBe("text");
18
- expect(fields[0]?.label).toBe("Name");
19
- });
5
+ describe("schemaToFields", () => {
6
+ describe("schemaToFieldConfigs", () => {
7
+ test("maps option schema to field configs", () => {
8
+ const schema: OptionSchema = {
9
+ name: { type: "string", description: "User name" },
10
+ color: {
11
+ type: "string",
12
+ description: "Color choice",
13
+ enum: ["red", "green", "blue"],
14
+ },
15
+ verbose: { type: "boolean", description: "Verbose output" },
16
+ count: { type: "number", description: "Count" },
17
+ files: { type: "array", description: "Files to process" },
18
+ repoPath: {
19
+ type: "string",
20
+ description: "Repository path",
21
+ label: "Repository",
22
+ },
23
+ hidden: { type: "string", description: "Hidden", tuiHidden: true },
24
+ path: { type: "string", description: "Path", placeholder: "Enter path here" },
25
+ grouped: { type: "string", description: "Grouped", group: "Basic" },
26
+ };
20
27
 
21
- test("converts string with enum to enum field", () => {
22
- const schema: OptionSchema = {
23
- color: {
24
- type: "string",
25
- description: "Color choice",
26
- enum: ["red", "green", "blue"],
27
- },
28
- };
29
-
30
- const fields = schemaToFieldConfigs(schema);
31
- expect(fields[0]?.type).toBe("enum");
32
- expect(fields[0]?.options).toHaveLength(3);
33
- expect(fields[0]?.options?.[0]?.name).toBe("red");
34
- });
28
+ const fields = schemaToFieldConfigs(schema);
35
29
 
36
- test("converts boolean type", () => {
37
- const schema: OptionSchema = {
38
- verbose: {
39
- type: "boolean",
40
- description: "Verbose output",
41
- },
42
- };
43
-
44
- const fields = schemaToFieldConfigs(schema);
45
- expect(fields[0]?.type).toBe("boolean");
46
- });
30
+ // Basic mappings
31
+ const name = fields.find((f) => f.key === "name");
32
+ expect(name).toMatchObject({ type: "text", label: "Name" });
47
33
 
48
- test("converts number type", () => {
49
- const schema: OptionSchema = {
50
- count: {
51
- type: "number",
52
- description: "Count",
53
- },
54
- };
55
-
56
- const fields = schemaToFieldConfigs(schema);
57
- expect(fields[0]?.type).toBe("number");
58
- });
34
+ const color = fields.find((f) => f.key === "color");
35
+ expect(color?.type).toBe("enum");
36
+ expect(color?.options?.length).toBe(3);
59
37
 
60
- test("converts array type to text", () => {
61
- const schema: OptionSchema = {
62
- files: {
63
- type: "array",
64
- description: "Files to process",
65
- },
66
- };
67
-
68
- const fields = schemaToFieldConfigs(schema);
69
- expect(fields[0]?.type).toBe("text");
70
- });
38
+ expect(fields.find((f) => f.key === "verbose")?.type).toBe("boolean");
39
+ expect(fields.find((f) => f.key === "count")?.type).toBe("number");
40
+ expect(fields.find((f) => f.key === "files")?.type).toBe("text");
71
41
 
72
- test("uses label from schema if provided", () => {
73
- const schema: OptionSchema = {
74
- repoPath: {
75
- type: "string",
76
- description: "Repository path",
77
- label: "Repository",
78
- },
79
- };
80
-
81
- const fields = schemaToFieldConfigs(schema);
82
- expect(fields[0]?.label).toBe("Repository");
42
+ // Decorations
43
+ expect(fields.find((f) => f.key === "repoPath")?.label).toBe("Repository");
44
+ expect(fields.some((f) => f.key === "hidden")).toBe(false);
45
+ expect(fields.find((f) => f.key === "path")?.placeholder).toBe("Enter path here");
46
+ expect(fields.find((f) => f.key === "grouped")?.group).toBe("Basic");
83
47
  });
84
48
 
85
49
  test("sorts by order", () => {
86
- const schema: OptionSchema = {
87
- third: { type: "string", description: "Third", order: 3 },
88
- first: { type: "string", description: "First", order: 1 },
89
- second: { type: "string", description: "Second", order: 2 },
90
- };
91
-
92
- const fields = schemaToFieldConfigs(schema);
93
- expect(fields[0]?.key).toBe("first");
94
- expect(fields[1]?.key).toBe("second");
95
- expect(fields[2]?.key).toBe("third");
96
- });
97
-
98
- test("excludes tuiHidden fields", () => {
99
- const schema: OptionSchema = {
100
- visible: { type: "string", description: "Visible" },
101
- hidden: { type: "string", description: "Hidden", tuiHidden: true },
102
- };
103
-
104
- const fields = schemaToFieldConfigs(schema);
105
- expect(fields).toHaveLength(1);
106
- expect(fields[0]?.key).toBe("visible");
107
- });
50
+ const schema: OptionSchema = {
51
+ third: { type: "string", description: "Third", order: 3 },
52
+ first: { type: "string", description: "First", order: 1 },
53
+ second: { type: "string", description: "Second", order: 2 },
54
+ };
108
55
 
109
- test("includes placeholder from schema", () => {
110
- const schema: OptionSchema = {
111
- path: {
112
- type: "string",
113
- description: "Path",
114
- placeholder: "Enter path here",
115
- },
116
- };
117
-
118
- const fields = schemaToFieldConfigs(schema);
119
- expect(fields[0]?.placeholder).toBe("Enter path here");
56
+ const fields = schemaToFieldConfigs(schema);
57
+ expect(fields.map((f) => f.key)).toEqual(["first", "second", "third"]);
120
58
  });
59
+ });
121
60
 
122
- test("includes group from schema", () => {
123
- const schema: OptionSchema = {
124
- name: {
125
- type: "string",
126
- description: "Name",
127
- group: "Basic",
128
- },
129
- };
130
-
131
- const fields = schemaToFieldConfigs(schema);
132
- expect(fields[0]?.group).toBe("Basic");
133
- });
134
- });
135
-
136
- describe("getFieldDisplayValue", () => {
137
- test("displays boolean as True/False", () => {
138
- const field = { key: "enabled", label: "Enabled", type: "boolean" as const };
139
- expect(getFieldDisplayValue(true, field)).toBe("True");
140
- expect(getFieldDisplayValue(false, field)).toBe("False");
141
- });
61
+ describe("getFieldDisplayValue", () => {
62
+ test("formats values for display", () => {
63
+ expect(getFieldDisplayValue(true, { key: "enabled", label: "Enabled", type: "boolean" })).toBe(
64
+ "True"
65
+ );
66
+ expect(getFieldDisplayValue(false, { key: "enabled", label: "Enabled", type: "boolean" })).toBe(
67
+ "False"
68
+ );
142
69
 
143
- test("displays enum option name", () => {
144
- const field = {
145
- key: "color",
146
- label: "Color",
147
- type: "enum" as const,
148
- options: [
149
- { name: "Red", value: "red" },
150
- { name: "Green", value: "green" },
151
- ],
152
- };
153
- expect(getFieldDisplayValue("red", field)).toBe("Red");
154
- expect(getFieldDisplayValue("green", field)).toBe("Green");
155
- });
156
-
157
- test("displays (empty) for empty strings", () => {
158
- const field = { key: "name", label: "Name", type: "text" as const };
159
- expect(getFieldDisplayValue("", field)).toBe("(empty)");
160
- expect(getFieldDisplayValue(null, field)).toBe("(empty)");
161
- expect(getFieldDisplayValue(undefined, field)).toBe("(empty)");
162
- });
70
+ const enumField = {
71
+ key: "color",
72
+ label: "Color",
73
+ type: "enum" as const,
74
+ options: [
75
+ { name: "Red", value: "red" },
76
+ { name: "Green", value: "green" },
77
+ ],
78
+ };
79
+ expect(getFieldDisplayValue("red", enumField)).toBe("Red");
163
80
 
164
- test("truncates long values", () => {
165
- const field = { key: "desc", label: "Description", type: "text" as const };
166
- const longValue = "a".repeat(100);
167
- const result = getFieldDisplayValue(longValue, field);
168
- expect(result.length).toBe(60);
169
- expect(result.endsWith("...")).toBe(true);
170
- });
81
+ const textField = { key: "name", label: "Name", type: "text" as const };
82
+ expect(getFieldDisplayValue("", textField)).toBe("(empty)");
83
+ expect(getFieldDisplayValue(null, textField)).toBe("(empty)");
84
+ expect(getFieldDisplayValue(undefined, textField)).toBe("(empty)");
85
+ expect(getFieldDisplayValue("hello", textField)).toBe("hello");
171
86
 
172
- test("displays short values as-is", () => {
173
- const field = { key: "name", label: "Name", type: "text" as const };
174
- expect(getFieldDisplayValue("hello", field)).toBe("hello");
87
+ const longValue = "a".repeat(100);
88
+ const longResult = getFieldDisplayValue(longValue, { key: "desc", label: "Description", type: "text" });
89
+ expect(longResult.length).toBe(60);
90
+ expect(longResult.endsWith("...")).toBe(true);
175
91
  });
92
+ });
176
93
  });
@@ -1,7 +1,8 @@
1
1
  import { Command, type AnyCommand } from "../core/command.ts";
2
- import type { AppContext } from "../core/context.ts";
3
2
  import { generateCommandHelp, generateAppHelp } from "../core/help.ts";
3
+ import { KNOWN_COMMANDS } from "../core/knownCommands.ts";
4
4
  import type { OptionSchema } from "../types/command.ts";
5
+ import { GLOBAL_OPTIONS_SCHEMA } from "../core/application.ts";
5
6
 
6
7
  /**
7
8
  * Built-in help command that is auto-injected as a subcommand into all commands.
@@ -11,8 +12,9 @@ import type { OptionSchema } from "../types/command.ts";
11
12
  * be instantiated directly.
12
13
  */
13
14
  export class HelpCommand extends Command<OptionSchema> {
14
- readonly name = "help";
15
+ readonly name = KNOWN_COMMANDS.help;
15
16
  readonly description = "Show help for this command";
17
+ override readonly tuiHidden = true;
16
18
  readonly options = {} as const;
17
19
 
18
20
  private parentCommand: AnyCommand | null = null;
@@ -40,7 +42,7 @@ export class HelpCommand extends Command<OptionSchema> {
40
42
  return false;
41
43
  }
42
44
 
43
- override async execute(_ctx: AppContext): Promise<void> {
45
+ override async execute(): Promise<void> {
44
46
  let helpText: string;
45
47
 
46
48
  if (this.parentCommand) {
@@ -48,12 +50,18 @@ export class HelpCommand extends Command<OptionSchema> {
48
50
  helpText = generateCommandHelp(this.parentCommand, {
49
51
  appName: this.appName,
50
52
  version: this.appVersion,
53
+ globalOptionsSchema: GLOBAL_OPTIONS_SCHEMA,
51
54
  });
52
55
  } else {
53
56
  // Show help for the entire application
54
- helpText = generateAppHelp(this.allCommands, {
57
+ const visibleCommands = this.allCommands.filter(
58
+ (cmd) => !cmd.tuiHidden || cmd.supportsCli()
59
+ );
60
+
61
+ helpText = generateAppHelp(visibleCommands, {
55
62
  appName: this.appName,
56
63
  version: this.appVersion,
64
+ globalOptionsSchema: GLOBAL_OPTIONS_SCHEMA,
57
65
  });
58
66
  }
59
67
 
@@ -1,8 +1,10 @@
1
1
  import { Command } from "../core/command.ts";
2
- import type { AppContext } from "../core/context.ts";
2
+ import { AppContext } from "../core/context.ts";
3
3
  import { LogLevel } from "../core/logger.ts";
4
4
  import type { OptionSchema, OptionValues } from "../types/command.ts";
5
5
  import type { CommandResult } from "../core/command.ts";
6
+ import { getEnumKeys } from "../tui/utils/getEnumKeys.ts";
7
+ import { KNOWN_COMMANDS } from "../core/knownCommands.ts";
6
8
 
7
9
  /**
8
10
  * Options schema for the settings command.
@@ -12,7 +14,7 @@ const settingsOptions = {
12
14
  type: "string",
13
15
  description: "Minimum log level to emit",
14
16
  default: "info",
15
- enum: ["silly", "trace", "debug", "info", "warn", "error", "fatal"],
17
+ enum: getEnumKeys(LogLevel) as (keyof typeof LogLevel)[],
16
18
  label: "Log Level",
17
19
  order: 1,
18
20
  },
@@ -35,28 +37,6 @@ interface SettingsConfig {
35
37
  detailedLogs: boolean;
36
38
  }
37
39
 
38
- /**
39
- * Map of string log level names to LogLevel enum values.
40
- */
41
- const logLevelMap: Record<string, LogLevel> = {
42
- silly: LogLevel.Silly,
43
- trace: LogLevel.Trace,
44
- debug: LogLevel.Debug,
45
- info: LogLevel.Info,
46
- warn: LogLevel.Warn,
47
- error: LogLevel.Error,
48
- fatal: LogLevel.Fatal,
49
- };
50
-
51
- /**
52
- * Parse a string log level to the LogLevel enum.
53
- */
54
- function parseLogLevel(value?: string): LogLevel {
55
- if (!value) return LogLevel.Info;
56
- const level = logLevelMap[value.toLowerCase()];
57
- return level ?? LogLevel.Info;
58
- }
59
-
60
40
  /**
61
41
  * Built-in settings command for configuring logging.
62
42
  *
@@ -69,32 +49,38 @@ function parseLogLevel(value?: string): LogLevel {
69
49
  * In TUI mode, this command provides a UI for configuring these settings.
70
50
  */
71
51
  export class SettingsCommand extends Command<typeof settingsOptions, SettingsConfig> {
72
- readonly name = "settings";
52
+ override supportsCli(): boolean {
53
+ return false;
54
+ }
55
+
56
+ readonly name = KNOWN_COMMANDS.settings;
73
57
  override readonly displayName = "Settings";
58
+ override readonly tuiHidden = true;
74
59
  readonly description = "Configure logging level and output format";
75
60
  readonly options = settingsOptions;
76
61
 
77
62
  override readonly actionLabel = "Save Settings";
78
63
  override readonly immediateExecution = false;
79
64
 
80
- override buildConfig(_ctx: AppContext, opts: SettingsOptions): SettingsConfig {
81
- const logLevel = parseLogLevel(opts["log-level"] as string | undefined);
65
+ override buildConfig(opts: SettingsOptions): SettingsConfig {
66
+ const logLevelStr = opts["log-level"];
67
+ const logLevel = LogLevel[logLevelStr as keyof typeof LogLevel] ?? LogLevel.info;
82
68
  const detailedLogs = Boolean(opts["detailed-logs"]);
83
69
 
84
70
  return { logLevel, detailedLogs };
85
71
  }
86
72
 
87
- override async execute(ctx: AppContext, config: SettingsConfig): Promise<CommandResult> {
88
- this.applySettings(ctx, config);
73
+ override async execute(config: SettingsConfig): Promise<CommandResult> {
74
+ this.applySettings(config);
89
75
  return {
90
76
  success: true,
91
77
  message: `Logging set to ${LogLevel[config.logLevel]}${config.detailedLogs ? " with detailed format" : ""}`,
92
78
  };
93
79
  }
94
80
 
95
- private applySettings(ctx: AppContext, config: SettingsConfig): void {
96
- ctx.logger.setMinLevel(config.logLevel);
97
- ctx.logger.setDetailed(config.detailedLogs);
81
+ private applySettings(config: SettingsConfig): void {
82
+ AppContext.current.logger.setMinLevel(config.logLevel);
83
+ AppContext.current.logger.setDetailed(config.detailedLogs);
98
84
  }
99
85
  }
100
86
 
@@ -1,12 +1,11 @@
1
1
  import { Command } from "../core/command.ts";
2
- import type { AppContext } from "../core/context.ts";
3
2
  import { colors } from "../cli/output/colors.ts";
4
3
  import type { OptionSchema } from "../types/command.ts";
5
4
 
6
5
  /**
7
6
  * Configuration for version command.
8
7
  */
9
- export interface VersionConfig {
8
+ interface VersionConfig {
10
9
  /** Application name */
11
10
  appName: string;
12
11
  /** Application version (e.g., "1.0.0") */
@@ -33,6 +32,7 @@ export function formatVersion(version: string, commitHash?: string): string {
33
32
  export class VersionCommand extends Command<OptionSchema> {
34
33
  readonly name = "version";
35
34
  readonly description = "Show version information";
35
+ override readonly tuiHidden = true;
36
36
  readonly options = {} as const;
37
37
  readonly aliases = ["--version", "-v"];
38
38
 
@@ -54,7 +54,7 @@ export class VersionCommand extends Command<OptionSchema> {
54
54
  return formatVersion(this.appVersion, this.commitHash);
55
55
  }
56
56
 
57
- override async execute(_ctx: AppContext): Promise<void> {
57
+ override async execute(): Promise<void> {
58
58
  const versionDisplay = this.getFormattedVersion();
59
59
  console.log(`${colors.bold(this.appName)} ${colors.dim(`v${versionDisplay}`)}`);
60
60
  }
@@ -23,7 +23,7 @@ const BG_BLUE = "\x1b[44m";
23
23
  /**
24
24
  * Check if terminal supports colors
25
25
  */
26
- export function supportsColors(): boolean {
26
+ function supportsColors(): boolean {
27
27
  if (typeof process === "undefined") return false;
28
28
  if (process.env["NO_COLOR"]) return false;
29
29
  if (process.env["FORCE_COLOR"]) return true;