@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,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;
@@ -33,7 +35,14 @@ export class HelpCommand extends Command<OptionSchema> {
33
35
  this.appVersion = config.appVersion;
34
36
  }
35
37
 
36
- override async execute(_ctx: AppContext): Promise<void> {
38
+ /**
39
+ * Help command is CLI-only (auto-injected for CLI use, not shown in TUI).
40
+ */
41
+ override supportsTui(): boolean {
42
+ return false;
43
+ }
44
+
45
+ override async execute(): Promise<void> {
37
46
  let helpText: string;
38
47
 
39
48
  if (this.parentCommand) {
@@ -41,12 +50,18 @@ export class HelpCommand extends Command<OptionSchema> {
41
50
  helpText = generateCommandHelp(this.parentCommand, {
42
51
  appName: this.appName,
43
52
  version: this.appVersion,
53
+ globalOptionsSchema: GLOBAL_OPTIONS_SCHEMA,
44
54
  });
45
55
  } else {
46
56
  // Show help for the entire application
47
- helpText = generateAppHelp(this.allCommands, {
57
+ const visibleCommands = this.allCommands.filter(
58
+ (cmd) => !cmd.tuiHidden || cmd.supportsCli()
59
+ );
60
+
61
+ helpText = generateAppHelp(visibleCommands, {
48
62
  appName: this.appName,
49
63
  version: this.appVersion,
64
+ globalOptionsSchema: GLOBAL_OPTIONS_SCHEMA,
50
65
  });
51
66
  }
52
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") */
@@ -19,7 +18,7 @@ export interface VersionConfig {
19
18
  * Format version string with optional commit hash.
20
19
  * If commitHash is empty or undefined, shows "(dev)".
21
20
  */
22
- export function formatVersion(version: string, commitHash?: string): string {
21
+ function formatVersion(version: string, commitHash?: string): string {
23
22
  const hashPart = commitHash && commitHash.length > 0
24
23
  ? commitHash.substring(0, 7)
25
24
  : "(dev)";
@@ -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;
package/src/cli/parser.ts CHANGED
@@ -1,21 +1,6 @@
1
- import type { Command, OptionSchema, OptionValues } from "../types/command.ts";
2
- import { parseArgs, type ParseArgsConfig } from "util";
1
+ import type { OptionSchema, OptionValues } from "../types/command.ts";
2
+ import { type ParseArgsConfig } from "util";
3
3
 
4
- /**
5
- * Result of parsing CLI arguments
6
- */
7
- export interface ParseResult<T extends OptionSchema = OptionSchema> {
8
- command: Command<T> | null;
9
- commandPath: string[];
10
- options: OptionValues<T>;
11
- args: string[];
12
- showHelp: boolean;
13
- error?: ParseError;
14
- }
15
-
16
- /**
17
- * Error during parsing
18
- */
19
4
  export interface ParseError {
20
5
  type: "unknown_command" | "invalid_option" | "missing_required" | "validation";
21
6
  message: string;
@@ -30,21 +15,38 @@ export function extractCommandChain(args: string[]): {
30
15
  remaining: string[];
31
16
  } {
32
17
  const commands: string[] = [];
33
- let i = 0;
18
+ const remaining: string[] = [];
34
19
 
35
- for (; i < args.length; i++) {
20
+ for (let i = 0; i < args.length; i++) {
36
21
  const arg = args[i];
37
- if (arg?.startsWith("-")) {
38
- break;
22
+
23
+ if (!arg) {
24
+ continue;
39
25
  }
40
- if (arg) {
41
- commands.push(arg);
26
+
27
+ if (arg.startsWith("-")) {
28
+ remaining.push(arg);
29
+
30
+ const next = args[i + 1];
31
+ if (next && !next.startsWith("-")) {
32
+ remaining.push(next);
33
+ i += 1;
34
+ }
35
+
36
+ continue;
42
37
  }
38
+
39
+ if (remaining.length > 0) {
40
+ remaining.push(arg);
41
+ continue;
42
+ }
43
+
44
+ commands.push(arg);
43
45
  }
44
46
 
45
47
  return {
46
48
  commands,
47
- remaining: args.slice(i),
49
+ remaining,
48
50
  };
49
51
  }
50
52
 
@@ -168,74 +170,3 @@ export function validateOptions<T extends OptionSchema>(
168
170
  return errors;
169
171
  }
170
172
 
171
- interface ParseCliArgsOptions<T extends OptionSchema> {
172
- args: string[];
173
- commands: Record<string, Command<T>>;
174
- defaultCommand?: string;
175
- }
176
-
177
- /**
178
- * Parse CLI arguments into a result
179
- */
180
- export function parseCliArgs<T extends OptionSchema>(
181
- options: ParseCliArgsOptions<T>
182
- ): ParseResult<T> {
183
- const { args, commands, defaultCommand } = options;
184
- const { commands: commandChain, remaining } = extractCommandChain(args);
185
-
186
- // Check for help flag
187
- const showHelp = remaining.includes("--help") || remaining.includes("-h");
188
-
189
- // Find command
190
- const commandName = commandChain[0] ?? defaultCommand;
191
- if (!commandName) {
192
- return {
193
- command: null,
194
- commandPath: [],
195
- options: {} as OptionValues<T>,
196
- args: remaining,
197
- showHelp,
198
- };
199
- }
200
-
201
- const command = commands[commandName];
202
- if (!command) {
203
- return {
204
- command: null,
205
- commandPath: commandChain,
206
- options: {} as OptionValues<T>,
207
- args: remaining,
208
- showHelp,
209
- error: {
210
- type: "unknown_command",
211
- message: `Unknown command: ${commandName}`,
212
- },
213
- };
214
- }
215
-
216
- // Parse options
217
- const schema = command.options ?? ({} as T);
218
- const parseArgsConfig = schemaToParseArgsOptions(schema);
219
-
220
- let parsedValues: Record<string, unknown> = {};
221
- try {
222
- const { values } = parseArgs({
223
- args: remaining,
224
- ...parseArgsConfig,
225
- allowPositionals: false,
226
- });
227
- parsedValues = values;
228
- } catch {
229
- // Ignore parse errors for now
230
- }
231
-
232
- const optionValues = parseOptionValues(schema, parsedValues);
233
-
234
- return {
235
- command,
236
- commandPath: commandChain,
237
- options: optionValues,
238
- args: remaining,
239
- showHelp,
240
- };
241
- }