@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
@@ -3,7 +3,7 @@ import { AppContext, type AppConfig } from "../core/context.ts";
3
3
 
4
4
  describe("AppContext", () => {
5
5
  afterEach(() => {
6
- AppContext.clearCurrent();
6
+ AppContext.setCurrent(new AppContext({ name: "empty", version: "0.0.0" }));
7
7
  });
8
8
 
9
9
  describe("constructor", () => {
@@ -22,37 +22,12 @@ describe("AppContext", () => {
22
22
  });
23
23
 
24
24
  describe("static current", () => {
25
- test("throws when no current context", () => {
26
- expect(() => AppContext.current).toThrow(
27
- "AppContext.current accessed before initialization"
28
- );
29
- });
30
-
31
25
  test("returns current context after setCurrent", () => {
32
26
  const config: AppConfig = { name: "test", version: "1.0.0" };
33
27
  const ctx = new AppContext(config);
34
28
  AppContext.setCurrent(ctx);
35
29
  expect(AppContext.current).toBe(ctx);
36
30
  });
37
-
38
- test("hasCurrent returns false when no context", () => {
39
- expect(AppContext.hasCurrent()).toBe(false);
40
- });
41
-
42
- test("hasCurrent returns true after setCurrent", () => {
43
- const config: AppConfig = { name: "test", version: "1.0.0" };
44
- const ctx = new AppContext(config);
45
- AppContext.setCurrent(ctx);
46
- expect(AppContext.hasCurrent()).toBe(true);
47
- });
48
-
49
- test("clearCurrent clears the context", () => {
50
- const config: AppConfig = { name: "test", version: "1.0.0" };
51
- const ctx = new AppContext(config);
52
- AppContext.setCurrent(ctx);
53
- AppContext.clearCurrent();
54
- expect(AppContext.hasCurrent()).toBe(false);
55
- });
56
31
  });
57
32
 
58
33
  describe("services", () => {
@@ -0,0 +1,227 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ formatExamples,
4
+ formatOptionSchema,
5
+ formatOptions,
6
+ formatSubCommands,
7
+ formatUsage,
8
+ generateAppHelp,
9
+ generateCommandHelp,
10
+ } from "../core/help.ts";
11
+ import { Command } from "../core/command.ts";
12
+ import type { OptionSchema } from "../types/command.ts";
13
+
14
+ class SimpleCommand extends Command<OptionSchema> {
15
+ readonly name: string;
16
+ readonly description: string;
17
+ readonly options: OptionSchema;
18
+
19
+ constructor(config: { name: string; description: string; options?: OptionSchema }) {
20
+ super();
21
+ this.name = config.name;
22
+ this.description = config.description;
23
+ this.options = config.options ?? {};
24
+ }
25
+
26
+ override async execute(): Promise<void> {}
27
+ }
28
+
29
+ describe("Help Generation (core)", () => {
30
+ describe("formatUsage", () => {
31
+ test("formats basic usage and includes tokens", () => {
32
+ const cmd = new SimpleCommand({
33
+ name: "parent",
34
+ description: "Parent",
35
+ options: { verbose: { type: "boolean", description: "Verbose" } },
36
+ });
37
+ cmd.subCommands = [new SimpleCommand({ name: "child", description: "Child" })];
38
+
39
+ const usage = formatUsage(cmd, { appName: "myapp" });
40
+ expect(usage).toContain("myapp");
41
+ expect(usage).toContain("parent");
42
+ expect(usage).toContain("[command]");
43
+ expect(usage).toContain("[options]");
44
+ });
45
+ });
46
+
47
+ describe("formatSubCommands", () => {
48
+ test("formats subcommands list", () => {
49
+ const cmd = new SimpleCommand({ name: "parent", description: "Parent" });
50
+ cmd.subCommands = [new SimpleCommand({ name: "child", description: "Child command" })];
51
+
52
+ const commands = formatSubCommands(cmd);
53
+ expect(commands).toContain("child");
54
+ expect(commands).toContain("Child command");
55
+ });
56
+
57
+ test("returns empty for no subcommands", () => {
58
+ const cmd = new SimpleCommand({ name: "test", description: "Test" });
59
+ const commands = formatSubCommands(cmd);
60
+ expect(commands).toBe("");
61
+ });
62
+ });
63
+
64
+ describe("formatOptions", () => {
65
+ test("formats options with descriptions", () => {
66
+ const cmd = new SimpleCommand({
67
+ name: "test",
68
+ description: "Test",
69
+ options: { verbose: { type: "boolean", description: "Enable verbose" } },
70
+ });
71
+
72
+ const options = formatOptions(cmd);
73
+ expect(options).toContain("--verbose");
74
+ expect(options).toContain("Enable verbose");
75
+ });
76
+
77
+ test("shows option aliases", () => {
78
+ const cmd = new SimpleCommand({
79
+ name: "test",
80
+ description: "Test",
81
+ options: { verbose: { type: "boolean", alias: "v", description: "Verbose" } },
82
+ });
83
+
84
+ const options = formatOptions(cmd);
85
+ expect(options).toContain("-v");
86
+ });
87
+
88
+ test("shows default values", () => {
89
+ const cmd = new SimpleCommand({
90
+ name: "test",
91
+ description: "Test",
92
+ options: { count: { type: "number", default: 10, description: "Count" } },
93
+ });
94
+
95
+ const options = formatOptions(cmd);
96
+ expect(options).toContain("10");
97
+ });
98
+
99
+ test("shows required marker", () => {
100
+ const cmd = new SimpleCommand({
101
+ name: "test",
102
+ description: "Test",
103
+ options: { name: { type: "string", required: true, description: "Name" } },
104
+ });
105
+
106
+ const options = formatOptions(cmd);
107
+ expect(options).toContain("required");
108
+ });
109
+
110
+ test("shows enum values", () => {
111
+ const cmd = new SimpleCommand({
112
+ name: "test",
113
+ description: "Test",
114
+ options: {
115
+ level: {
116
+ type: "string",
117
+ enum: ["low", "high"],
118
+ description: "Level",
119
+ },
120
+ },
121
+ });
122
+
123
+ const options = formatOptions(cmd);
124
+ expect(options).toContain("low");
125
+ expect(options).toContain("high");
126
+ });
127
+
128
+ test("returns empty for no options", () => {
129
+ const cmd = new SimpleCommand({ name: "test", description: "Test" });
130
+ const options = formatOptions(cmd);
131
+ expect(options).toBe("");
132
+ });
133
+ });
134
+
135
+ describe("formatExamples", () => {
136
+ test("formats examples list and empty state", () => {
137
+ const cmd = new SimpleCommand({ name: "test", description: "Test" });
138
+ cmd.examples = [{ command: "test --verbose", description: "Run with verbose" }];
139
+
140
+ expect(formatExamples(cmd)).toContain("test --verbose");
141
+ expect(formatExamples(cmd)).toContain("Run with verbose");
142
+
143
+ const noExamples = new SimpleCommand({ name: "empty", description: "Empty" });
144
+ expect(formatExamples(noExamples)).toBe("");
145
+ });
146
+ });
147
+
148
+ describe("generateCommandHelp", () => {
149
+ test("includes usage, description, and options section", () => {
150
+ const cmd = new SimpleCommand({
151
+ name: "test",
152
+ description: "A test command for testing",
153
+ options: { verbose: { type: "boolean", description: "Verbose mode" } },
154
+ });
155
+
156
+ const help = generateCommandHelp(cmd, { appName: "myapp" });
157
+ expect(help).toContain("Usage:");
158
+ expect(help).toContain("A test command for testing");
159
+ expect(help).toContain("Options:");
160
+ expect(help).toContain("--verbose");
161
+ expect(help).toContain("--verbose, --no-verbose");
162
+ });
163
+ });
164
+
165
+ describe("generateAppHelp", () => {
166
+ test("generates root help with commands", () => {
167
+ const commands = [new SimpleCommand({ name: "run", description: "Run something" })];
168
+ const help = generateAppHelp(commands, { appName: "myapp" });
169
+ expect(help).toContain("Commands:");
170
+ expect(help).toContain("run");
171
+ });
172
+ });
173
+
174
+ describe("Global Options section", () => {
175
+ test("modes via schema and includes mode", () => {
176
+ const global = formatOptionSchema("Global Options", {
177
+ "log-level": { type: "string", description: "Minimum log level" },
178
+ mode: {
179
+ type: "string",
180
+ enum: ["opentui", "ink", "cli", "default"],
181
+ default: "default",
182
+ description: "Mode",
183
+ },
184
+ });
185
+
186
+ expect(global).toContain("Global Options");
187
+ expect(global).toContain("--log-level");
188
+ expect(global).toContain("--mode");
189
+ expect(global).toContain("opentui");
190
+ expect(global).toContain("ink");
191
+ expect(global).toContain("cli");
192
+ expect(global).toContain("default");
193
+ expect(global).toContain("[default: default]");
194
+ });
195
+
196
+ test("generateCommandHelp includes global options when provided", () => {
197
+ const cmd = new SimpleCommand({ name: "test", description: "Test" });
198
+ const help = generateCommandHelp(cmd, {
199
+ appName: "myapp",
200
+ globalOptionsSchema: {
201
+ mode: {
202
+ type: "string",
203
+ enum: ["opentui", "ink", "cli", "default"],
204
+ default: "default",
205
+ description: "Mode",
206
+ },
207
+ },
208
+ });
209
+
210
+ expect(help).toContain("Global Options");
211
+ expect(help).toContain("--mode");
212
+ });
213
+
214
+ test("generateAppHelp includes global options when provided", () => {
215
+ const commands = [new SimpleCommand({ name: "run", description: "Run something" })];
216
+ const help = generateAppHelp(commands, {
217
+ appName: "myapp",
218
+ globalOptionsSchema: {
219
+ mode: { type: "string", enum: ["opentui", "ink", "cli", "default"], description: "Mode" },
220
+ },
221
+ });
222
+
223
+ expect(help).toContain("Global Options");
224
+ expect(help).toContain("--mode");
225
+ });
226
+ });
227
+ });
@@ -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
+