@pablozaiden/terminatui 0.2.0 → 0.3.0-beta-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/AGENTS.md +14 -2
  2. package/CLAUDE.md +1 -0
  3. package/README.md +64 -43
  4. package/bun.lock +85 -0
  5. package/examples/tui-app/commands/config/app/get.ts +6 -10
  6. package/examples/tui-app/commands/config/app/index.ts +2 -6
  7. package/examples/tui-app/commands/config/app/set.ts +23 -13
  8. package/examples/tui-app/commands/config/index.ts +2 -6
  9. package/examples/tui-app/commands/config/user/get.ts +6 -10
  10. package/examples/tui-app/commands/config/user/index.ts +2 -6
  11. package/examples/tui-app/commands/config/user/set.ts +6 -10
  12. package/examples/tui-app/commands/greet.ts +13 -11
  13. package/examples/tui-app/commands/math.ts +5 -9
  14. package/examples/tui-app/commands/status.ts +21 -12
  15. package/examples/tui-app/index.ts +6 -3
  16. package/guides/01-hello-world.md +7 -2
  17. package/guides/02-adding-options.md +2 -2
  18. package/guides/03-multiple-commands.md +6 -8
  19. package/guides/04-subcommands.md +8 -8
  20. package/guides/05-interactive-tui.md +45 -30
  21. package/guides/06-config-validation.md +4 -12
  22. package/guides/07-async-cancellation.md +14 -16
  23. package/guides/08-complete-application.md +12 -42
  24. package/guides/README.md +7 -3
  25. package/package.json +4 -8
  26. package/src/__tests__/application.test.ts +87 -68
  27. package/src/__tests__/buildCliCommand.test.ts +99 -119
  28. package/src/__tests__/builtins.test.ts +27 -75
  29. package/src/__tests__/command.test.ts +100 -131
  30. package/src/__tests__/context.test.ts +1 -26
  31. package/src/__tests__/helpCore.test.ts +227 -0
  32. package/src/__tests__/parser.test.ts +98 -244
  33. package/src/__tests__/registry.test.ts +33 -160
  34. package/src/__tests__/schemaToFields.test.ts +75 -158
  35. package/src/builtins/help.ts +12 -4
  36. package/src/builtins/settings.ts +18 -32
  37. package/src/builtins/version.ts +4 -4
  38. package/src/cli/output/colors.ts +1 -1
  39. package/src/cli/parser.ts +26 -95
  40. package/src/core/application.ts +192 -110
  41. package/src/core/command.ts +26 -9
  42. package/src/core/context.ts +31 -20
  43. package/src/core/help.ts +24 -18
  44. package/src/core/knownCommands.ts +13 -0
  45. package/src/core/logger.ts +39 -42
  46. package/src/core/registry.ts +5 -12
  47. package/src/tui/TuiApplication.tsx +63 -120
  48. package/src/tui/TuiRoot.tsx +135 -0
  49. package/src/tui/adapters/factory.ts +19 -0
  50. package/src/tui/adapters/ink/InkRenderer.tsx +135 -0
  51. package/src/tui/adapters/ink/components/Button.tsx +12 -0
  52. package/src/tui/adapters/ink/components/Code.tsx +6 -0
  53. package/src/tui/adapters/ink/components/CodeHighlight.tsx +6 -0
  54. package/src/tui/adapters/ink/components/Container.tsx +5 -0
  55. package/src/tui/adapters/ink/components/Field.tsx +12 -0
  56. package/src/tui/adapters/ink/components/Label.tsx +24 -0
  57. package/src/tui/adapters/ink/components/MenuButton.tsx +12 -0
  58. package/src/tui/adapters/ink/components/MenuItem.tsx +17 -0
  59. package/src/tui/adapters/ink/components/Overlay.tsx +5 -0
  60. package/src/tui/adapters/ink/components/Panel.tsx +15 -0
  61. package/src/tui/adapters/ink/components/ScrollView.tsx +5 -0
  62. package/src/tui/adapters/ink/components/Select.tsx +44 -0
  63. package/src/tui/adapters/ink/components/Spacer.tsx +15 -0
  64. package/src/tui/adapters/ink/components/Spinner.tsx +5 -0
  65. package/src/tui/adapters/ink/components/TextInput.tsx +22 -0
  66. package/src/tui/adapters/ink/components/Value.tsx +7 -0
  67. package/src/tui/adapters/ink/keyboard.ts +97 -0
  68. package/src/tui/adapters/ink/utils.ts +16 -0
  69. package/src/tui/adapters/opentui/OpenTuiRenderer.tsx +115 -0
  70. package/src/tui/adapters/opentui/components/Button.tsx +13 -0
  71. package/src/tui/adapters/opentui/components/Code.tsx +12 -0
  72. package/src/tui/adapters/opentui/components/CodeHighlight.tsx +24 -0
  73. package/src/tui/adapters/opentui/components/Container.tsx +56 -0
  74. package/src/tui/adapters/opentui/components/Field.tsx +18 -0
  75. package/src/tui/adapters/opentui/components/Label.tsx +15 -0
  76. package/src/tui/adapters/opentui/components/MenuButton.tsx +14 -0
  77. package/src/tui/adapters/opentui/components/MenuItem.tsx +29 -0
  78. package/src/tui/adapters/opentui/components/Overlay.tsx +21 -0
  79. package/src/tui/adapters/opentui/components/Panel.tsx +78 -0
  80. package/src/tui/adapters/opentui/components/ScrollView.tsx +85 -0
  81. package/src/tui/adapters/opentui/components/Select.tsx +59 -0
  82. package/src/tui/adapters/opentui/components/Spacer.tsx +5 -0
  83. package/src/tui/adapters/opentui/components/Spinner.tsx +12 -0
  84. package/src/tui/adapters/opentui/components/TextInput.tsx +13 -0
  85. package/src/tui/adapters/opentui/components/Value.tsx +13 -0
  86. package/src/tui/{hooks → adapters/opentui/hooks}/useSpinner.ts +2 -11
  87. package/src/tui/adapters/opentui/keyboard.ts +61 -0
  88. package/src/tui/adapters/types.ts +70 -0
  89. package/src/tui/components/ActionButton.tsx +0 -36
  90. package/src/tui/components/CommandSelector.tsx +45 -92
  91. package/src/tui/components/ConfigForm.tsx +68 -42
  92. package/src/tui/components/FieldRow.tsx +0 -30
  93. package/src/tui/components/Header.tsx +14 -13
  94. package/src/tui/components/JsonHighlight.tsx +10 -17
  95. package/src/tui/components/ModalBase.tsx +38 -0
  96. package/src/tui/components/ResultsPanel.tsx +27 -36
  97. package/src/tui/components/StatusBar.tsx +24 -39
  98. package/src/tui/components/logColors.ts +12 -0
  99. package/src/tui/context/ClipboardContext.tsx +87 -0
  100. package/src/tui/context/ExecutorContext.tsx +139 -0
  101. package/src/tui/context/KeyboardContext.tsx +85 -71
  102. package/src/tui/context/LogsContext.tsx +35 -0
  103. package/src/tui/context/NavigationContext.tsx +194 -0
  104. package/src/tui/context/RendererContext.tsx +20 -0
  105. package/src/tui/context/TuiAppContext.tsx +58 -0
  106. package/src/tui/hooks/useActiveKeyHandler.ts +75 -0
  107. package/src/tui/hooks/useBackHandler.ts +34 -0
  108. package/src/tui/hooks/useClipboard.ts +40 -25
  109. package/src/tui/hooks/useClipboardProvider.ts +42 -0
  110. package/src/tui/hooks/useGlobalKeyHandler.ts +54 -0
  111. package/src/tui/modals/CliModal.tsx +82 -0
  112. package/src/tui/modals/EditorModal.tsx +207 -0
  113. package/src/tui/modals/LogsModal.tsx +98 -0
  114. package/src/tui/registry.ts +102 -0
  115. package/src/tui/screens/CommandSelectScreen.tsx +162 -0
  116. package/src/tui/screens/ConfigScreen.tsx +160 -0
  117. package/src/tui/screens/ErrorScreen.tsx +58 -0
  118. package/src/tui/screens/ResultsScreen.tsx +60 -0
  119. package/src/tui/screens/RunningScreen.tsx +72 -0
  120. package/src/tui/screens/ScreenBase.ts +6 -0
  121. package/src/tui/semantic/Button.tsx +7 -0
  122. package/src/tui/semantic/Code.tsx +7 -0
  123. package/src/tui/semantic/CodeHighlight.tsx +7 -0
  124. package/src/tui/semantic/Container.tsx +7 -0
  125. package/src/tui/semantic/Field.tsx +7 -0
  126. package/src/tui/semantic/Label.tsx +7 -0
  127. package/src/tui/semantic/MenuButton.tsx +7 -0
  128. package/src/tui/semantic/MenuItem.tsx +7 -0
  129. package/src/tui/semantic/Overlay.tsx +7 -0
  130. package/src/tui/semantic/Panel.tsx +7 -0
  131. package/src/tui/semantic/ScrollView.tsx +9 -0
  132. package/src/tui/semantic/Select.tsx +7 -0
  133. package/src/tui/semantic/Spacer.tsx +7 -0
  134. package/src/tui/semantic/Spinner.tsx +7 -0
  135. package/src/tui/semantic/TextInput.tsx +7 -0
  136. package/src/tui/semantic/Value.tsx +7 -0
  137. package/src/tui/semantic/types.ts +195 -0
  138. package/src/tui/theme.ts +25 -14
  139. package/src/tui/utils/buildCliCommand.ts +1 -0
  140. package/src/tui/utils/getEnumKeys.ts +3 -0
  141. package/src/tui/utils/parameterPersistence.ts +1 -0
  142. package/src/types/command.ts +0 -60
  143. package/examples/tui-app/commands/index.ts +0 -4
  144. package/src/__tests__/colors.test.ts +0 -127
  145. package/src/__tests__/commandClass.test.ts +0 -130
  146. package/src/__tests__/help.test.ts +0 -412
  147. package/src/__tests__/registryNew.test.ts +0 -160
  148. package/src/__tests__/table.test.ts +0 -146
  149. package/src/__tests__/tui.test.ts +0 -26
  150. package/src/builtins/index.ts +0 -4
  151. package/src/cli/help.ts +0 -174
  152. package/src/cli/index.ts +0 -3
  153. package/src/cli/output/index.ts +0 -2
  154. package/src/cli/output/table.ts +0 -141
  155. package/src/commands/help.ts +0 -50
  156. package/src/commands/index.ts +0 -1
  157. package/src/components/index.ts +0 -147
  158. package/src/core/index.ts +0 -15
  159. package/src/hooks/index.ts +0 -131
  160. package/src/index.ts +0 -137
  161. package/src/registry/commandRegistry.ts +0 -77
  162. package/src/registry/index.ts +0 -1
  163. package/src/tui/TuiApp.tsx +0 -619
  164. package/src/tui/app.ts +0 -29
  165. package/src/tui/components/CliModal.tsx +0 -81
  166. package/src/tui/components/EditorModal.tsx +0 -177
  167. package/src/tui/components/LogsPanel.tsx +0 -86
  168. package/src/tui/components/index.ts +0 -13
  169. package/src/tui/context/index.ts +0 -7
  170. package/src/tui/hooks/index.ts +0 -35
  171. package/src/tui/hooks/useKeyboardHandler.ts +0 -91
  172. package/src/tui/hooks/useLogStream.ts +0 -96
  173. package/src/tui/index.ts +0 -65
  174. package/src/tui/utils/index.ts +0 -13
  175. package/src/types/index.ts +0 -1
@@ -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
+ });
@@ -1,268 +1,122 @@
1
- import { test, expect, describe } from "bun:test";
1
+ import { describe, expect, test } from "bun:test";
2
2
  import {
3
3
  extractCommandChain,
4
- schemaToParseArgsOptions,
5
4
  parseOptionValues,
5
+ schemaToParseArgsOptions,
6
6
  validateOptions,
7
- parseCliArgs,
8
7
  } from "../cli/parser.ts";
9
8
  import type { OptionSchema } from "../types/command.ts";
10
- import { defineCommand } from "../types/command.ts";
11
-
12
- describe("extractCommandChain", () => {
13
- test("extracts command path with no flags", () => {
14
- const result = extractCommandChain(["run", "test"]);
15
- expect(result.commands).toEqual(["run", "test"]);
16
- expect(result.remaining).toEqual([]);
17
- });
18
-
19
- test("separates commands from flags", () => {
20
- const result = extractCommandChain(["run", "--verbose", "file.ts"]);
21
- expect(result.commands).toEqual(["run"]);
22
- expect(result.remaining).toEqual(["--verbose", "file.ts"]);
23
- });
24
-
25
- test("handles args starting with flags", () => {
26
- const result = extractCommandChain(["--help"]);
27
- expect(result.commands).toEqual([]);
28
- expect(result.remaining).toEqual(["--help"]);
29
- });
30
-
31
- test("handles short flags", () => {
32
- const result = extractCommandChain(["run", "-v"]);
33
- expect(result.commands).toEqual(["run"]);
34
- expect(result.remaining).toEqual(["-v"]);
35
- });
36
-
37
- test("handles empty args", () => {
38
- const result = extractCommandChain([]);
39
- expect(result.commands).toEqual([]);
40
- expect(result.remaining).toEqual([]);
41
- });
42
-
43
- test("extracts nested command path", () => {
44
- const result = extractCommandChain(["config", "set", "--key", "value"]);
45
- expect(result.commands).toEqual(["config", "set"]);
46
- expect(result.remaining).toEqual(["--key", "value"]);
47
- });
48
- });
49
-
50
- describe("schemaToParseArgsOptions", () => {
51
- test("converts string option", () => {
52
- const schema: OptionSchema = {
53
- name: { type: "string", description: "Name" },
54
- };
55
- const result = schemaToParseArgsOptions(schema);
56
- expect(result.options!["name"]?.type).toBe("string");
57
- });
58
-
59
- test("converts boolean option", () => {
60
- const schema: OptionSchema = {
61
- verbose: { type: "boolean", description: "Verbose" },
62
- };
63
- const result = schemaToParseArgsOptions(schema);
64
- expect(result.options!["verbose"]?.type).toBe("boolean");
65
- });
66
-
67
- test("converts alias to short", () => {
68
- const schema: OptionSchema = {
69
- verbose: { type: "boolean", alias: "v", description: "Verbose" },
70
- };
71
- const result = schemaToParseArgsOptions(schema);
72
- expect(result.options!["verbose"]?.short).toBe("v");
73
- });
74
-
75
- test("converts array option to multiple", () => {
76
- const schema: OptionSchema = {
77
- files: { type: "array", description: "Files" },
78
- };
79
- const result = schemaToParseArgsOptions(schema);
80
- expect(result.options!["files"]?.multiple).toBe(true);
81
- });
82
-
83
- test("includes default values", () => {
84
- const schema: OptionSchema = {
85
- count: { type: "number", default: 10, description: "Count" },
86
- };
87
- const result = schemaToParseArgsOptions(schema);
88
- // parseArgs expects string defaults for non-boolean types
89
- expect(result.options!["count"]?.default).toBe("10");
90
- });
91
-
92
- test("includes default values for boolean", () => {
93
- const schema: OptionSchema = {
94
- verbose: { type: "boolean", default: false, description: "Verbose" },
95
- };
96
- const result = schemaToParseArgsOptions(schema);
97
- // Boolean defaults remain as boolean
98
- expect(result.options!["verbose"]?.default).toBe(false);
99
- });
100
- });
101
-
102
- describe("parseOptionValues", () => {
103
- test("passes through string values", () => {
104
- const schema: OptionSchema = {
105
- name: { type: "string", description: "Name" },
106
- };
107
- const result = parseOptionValues(schema, { name: "test" });
108
- expect(result["name"]).toBe("test");
109
- });
110
-
111
- test("coerces number values", () => {
112
- const schema: OptionSchema = {
113
- count: { type: "number", description: "Count" },
114
- };
115
- const result = parseOptionValues(schema, { count: "42" });
116
- expect(result["count"]).toBe(42);
117
- });
118
9
 
119
- test("coerces boolean values", () => {
120
- const schema: OptionSchema = {
121
- verbose: { type: "boolean", description: "Verbose" },
122
- };
123
- const result = parseOptionValues(schema, { verbose: "true" });
124
- expect(result["verbose"]).toBe(true);
125
- });
126
-
127
- test("applies default values", () => {
128
- const schema: OptionSchema = {
129
- count: { type: "number", default: 5, description: "Count" },
130
- };
131
- const result = parseOptionValues(schema, {});
132
- expect(result["count"]).toBe(5);
133
- });
134
-
135
- test("reads from environment variables", () => {
136
- process.env["TEST_VALUE"] = "env-value";
137
- const schema: OptionSchema = {
138
- value: { type: "string", env: "TEST_VALUE", description: "Value" },
139
- };
140
- const result = parseOptionValues(schema, {});
141
- expect(result["value"]).toBe("env-value");
142
- delete process.env["TEST_VALUE"];
143
- });
144
-
145
- test("validates enum values", () => {
146
- const schema: OptionSchema = {
147
- level: {
148
- type: "string",
149
- enum: ["low", "medium", "high"],
150
- description: "Level",
151
- },
152
- };
153
- const result = parseOptionValues(schema, { level: "medium" });
154
- expect(result["level"]).toBe("medium");
155
- });
156
-
157
- test("throws on invalid enum value", () => {
158
- const schema: OptionSchema = {
159
- level: {
160
- type: "string",
161
- enum: ["low", "medium", "high"],
162
- description: "Level",
163
- },
164
- };
165
- expect(() => parseOptionValues(schema, { level: "invalid" })).toThrow();
166
- });
167
- });
168
-
169
- describe("validateOptions", () => {
170
- test("returns empty array for valid options", () => {
171
- const schema: OptionSchema = {
172
- name: { type: "string", description: "Name" },
173
- };
174
- const errors = validateOptions(schema, { name: "test" });
175
- expect(errors).toEqual([]);
176
- });
177
-
178
- test("returns error for missing required option", () => {
179
- const schema: OptionSchema = {
180
- name: { type: "string", required: true, description: "Name" },
181
- };
182
- const errors = validateOptions(schema, {} as Record<string, unknown>);
183
- expect(errors.length).toBeGreaterThan(0);
184
- expect(errors[0]?.type).toBe("missing_required");
185
- });
186
-
187
- test("validates number min/max", () => {
188
- const schema: OptionSchema = {
189
- count: { type: "number", min: 1, max: 10, description: "Count" },
190
- };
191
- const errors = validateOptions(schema, { count: 0 });
192
- expect(errors.length).toBeGreaterThan(0);
193
- expect(errors[0]?.type).toBe("validation");
194
- });
195
- });
196
-
197
- describe("parseCliArgs", () => {
198
- test("parses command name", () => {
199
- const cmd = defineCommand({
200
- name: "run",
201
- description: "Run command",
202
- execute: () => {},
203
- });
204
-
205
- const result = parseCliArgs({
206
- args: ["run"],
207
- commands: { run: cmd },
10
+ describe("cli/parser helpers", () => {
11
+ describe("extractCommandChain", () => {
12
+ test("splits commands from flags", () => {
13
+ const cases: Array<{
14
+ args: string[];
15
+ expected: { commands: string[]; remaining: string[] };
16
+ }> = [
17
+ { args: ["run", "test"], expected: { commands: ["run", "test"], remaining: [] } },
18
+ {
19
+ args: ["run", "--verbose", "file.ts"],
20
+ expected: { commands: ["run"], remaining: ["--verbose", "file.ts"] },
21
+ },
22
+ { args: ["--help"], expected: { commands: [], remaining: ["--help"] } },
23
+ { args: ["run", "-v"], expected: { commands: ["run"], remaining: ["-v"] } },
24
+ { args: [], expected: { commands: [], remaining: [] } },
25
+ {
26
+ args: ["config", "set", "--key", "value"],
27
+ expected: { commands: ["config", "set"], remaining: ["--key", "value"] },
28
+ },
29
+ ];
30
+
31
+ for (const c of cases) {
32
+ expect(extractCommandChain(c.args)).toEqual(c.expected);
33
+ }
208
34
  });
209
-
210
- expect(result.command).toBe(cmd);
211
- expect(result.commandPath).toEqual(["run"]);
212
35
  });
213
36
 
214
- test("detects help flag", () => {
215
- const cmd = defineCommand({
216
- name: "run",
217
- description: "Run command",
218
- execute: () => {},
219
- });
220
-
221
- const result = parseCliArgs({
222
- args: ["run", "--help"],
223
- commands: { run: cmd },
37
+ describe("schemaToParseArgsOptions", () => {
38
+ test("converts OptionSchema to parseArgs config", () => {
39
+ const schema: OptionSchema = {
40
+ name: { type: "string", description: "Name" },
41
+ verbose: { type: "boolean", alias: "v", default: false, description: "Verbose" },
42
+ files: { type: "array", description: "Files" },
43
+ count: { type: "number", default: 10, description: "Count" },
44
+ };
45
+
46
+ const result = schemaToParseArgsOptions(schema);
47
+
48
+ expect(result.options?.["name"]).toMatchObject({ type: "string" });
49
+ expect(result.options?.["verbose"]).toMatchObject({
50
+ type: "boolean",
51
+ short: "v",
52
+ default: false,
53
+ });
54
+ expect(result.options?.["files"]).toMatchObject({ multiple: true });
55
+ // parseArgs expects string defaults for non-boolean types
56
+ expect(result.options?.["count"]).toMatchObject({ type: "string", default: "10" });
224
57
  });
225
-
226
- expect(result.showHelp).toBe(true);
227
58
  });
228
59
 
229
- test("detects -h flag", () => {
230
- const cmd = defineCommand({
231
- name: "run",
232
- description: "Run command",
233
- execute: () => {},
60
+ describe("parseOptionValues", () => {
61
+ test("coerces values, applies defaults, and reads env", () => {
62
+ process.env["TEST_VALUE"] = "env-value";
63
+
64
+ const schema: OptionSchema = {
65
+ name: { type: "string", description: "Name" },
66
+ count: { type: "number", default: 5, description: "Count" },
67
+ verbose: { type: "boolean", description: "Verbose" },
68
+ value: { type: "string", env: "TEST_VALUE", description: "Value" },
69
+ level: {
70
+ type: "string",
71
+ enum: ["low", "medium", "high"],
72
+ description: "Level",
73
+ },
74
+ };
75
+
76
+ const result = parseOptionValues(schema, {
77
+ name: "test",
78
+ count: "42",
79
+ verbose: "true",
80
+ level: "medium",
81
+ });
82
+
83
+ expect(result["name"]).toBe("test");
84
+ expect(result["count"]).toBe(42);
85
+ expect(result["verbose"]).toBe(true);
86
+ expect(result["value"]).toBe("env-value");
87
+ expect(result["level"]).toBe("medium");
88
+
89
+ delete process.env["TEST_VALUE"];
234
90
  });
235
91
 
236
- const result = parseCliArgs({
237
- args: ["run", "-h"],
238
- commands: { run: cmd },
239
- });
92
+ test("throws on invalid enum", () => {
93
+ const schema: OptionSchema = {
94
+ level: {
95
+ type: "string",
96
+ enum: ["low", "medium", "high"],
97
+ description: "Level",
98
+ },
99
+ };
240
100
 
241
- expect(result.showHelp).toBe(true);
101
+ expect(() => parseOptionValues(schema, { level: "invalid" })).toThrow();
102
+ });
242
103
  });
243
104
 
244
- test("returns error for unknown command", () => {
245
- const result = parseCliArgs({
246
- args: ["unknown"],
247
- commands: {},
248
- });
105
+ describe("validateOptions", () => {
106
+ test("returns errors for missing required and invalid ranges", () => {
107
+ const schema: OptionSchema = {
108
+ name: { type: "string", required: true, description: "Name" },
109
+ count: { type: "number", min: 1, max: 10, description: "Count" },
110
+ };
249
111
 
250
- expect(result.error?.type).toBe("unknown_command");
251
- });
112
+ expect(validateOptions(schema, { name: "ok", count: 5 })).toEqual([]);
252
113
 
253
- test("uses default command if provided", () => {
254
- const cmd = defineCommand({
255
- name: "default",
256
- description: "Default command",
257
- execute: () => {},
258
- });
114
+ const missing = validateOptions(schema, {} as Record<string, unknown>);
115
+ expect(missing.some((e) => e.type === "missing_required" && e.field === "name")).toBe(true);
259
116
 
260
- const result = parseCliArgs({
261
- args: [],
262
- commands: { default: cmd },
263
- defaultCommand: "default",
117
+ const tooLow = validateOptions(schema, { name: "ok", count: 0 });
118
+ expect(tooLow.some((e) => e.type === "validation" && e.field === "count")).toBe(true);
264
119
  });
265
-
266
- expect(result.command).toBe(cmd);
267
120
  });
268
121
  });
122
+