@pablozaiden/terminatui 0.1.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 (95) hide show
  1. package/.devcontainer/devcontainer.json +19 -0
  2. package/.devcontainer/install-prerequisites.sh +49 -0
  3. package/.github/workflows/copilot-setup-steps.yml +32 -0
  4. package/.github/workflows/pull-request.yml +27 -0
  5. package/.github/workflows/release-npm-package.yml +78 -0
  6. package/LICENSE +21 -0
  7. package/README.md +524 -0
  8. package/examples/tui-app/commands/greet.ts +75 -0
  9. package/examples/tui-app/commands/index.ts +3 -0
  10. package/examples/tui-app/commands/math.ts +114 -0
  11. package/examples/tui-app/commands/status.ts +75 -0
  12. package/examples/tui-app/index.ts +34 -0
  13. package/guides/01-hello-world.md +96 -0
  14. package/guides/02-adding-options.md +103 -0
  15. package/guides/03-multiple-commands.md +163 -0
  16. package/guides/04-subcommands.md +206 -0
  17. package/guides/05-interactive-tui.md +194 -0
  18. package/guides/06-config-validation.md +264 -0
  19. package/guides/07-async-cancellation.md +388 -0
  20. package/guides/08-complete-application.md +673 -0
  21. package/guides/README.md +74 -0
  22. package/package.json +32 -0
  23. package/src/__tests__/application.test.ts +425 -0
  24. package/src/__tests__/buildCliCommand.test.ts +125 -0
  25. package/src/__tests__/builtins.test.ts +133 -0
  26. package/src/__tests__/colors.test.ts +127 -0
  27. package/src/__tests__/command.test.ts +157 -0
  28. package/src/__tests__/commandClass.test.ts +130 -0
  29. package/src/__tests__/context.test.ts +97 -0
  30. package/src/__tests__/help.test.ts +412 -0
  31. package/src/__tests__/parser.test.ts +268 -0
  32. package/src/__tests__/registry.test.ts +195 -0
  33. package/src/__tests__/registryNew.test.ts +160 -0
  34. package/src/__tests__/schemaToFields.test.ts +176 -0
  35. package/src/__tests__/table.test.ts +146 -0
  36. package/src/__tests__/tui.test.ts +26 -0
  37. package/src/builtins/help.ts +85 -0
  38. package/src/builtins/index.ts +4 -0
  39. package/src/builtins/settings.ts +106 -0
  40. package/src/builtins/version.ts +72 -0
  41. package/src/cli/help.ts +174 -0
  42. package/src/cli/index.ts +3 -0
  43. package/src/cli/output/colors.ts +74 -0
  44. package/src/cli/output/index.ts +2 -0
  45. package/src/cli/output/table.ts +141 -0
  46. package/src/cli/parser.ts +241 -0
  47. package/src/commands/help.ts +50 -0
  48. package/src/commands/index.ts +1 -0
  49. package/src/components/index.ts +147 -0
  50. package/src/core/application.ts +461 -0
  51. package/src/core/command.ts +269 -0
  52. package/src/core/context.ts +112 -0
  53. package/src/core/help.ts +214 -0
  54. package/src/core/index.ts +15 -0
  55. package/src/core/logger.ts +164 -0
  56. package/src/core/registry.ts +140 -0
  57. package/src/hooks/index.ts +131 -0
  58. package/src/index.ts +137 -0
  59. package/src/registry/commandRegistry.ts +77 -0
  60. package/src/registry/index.ts +1 -0
  61. package/src/tui/TuiApp.tsx +582 -0
  62. package/src/tui/TuiApplication.tsx +230 -0
  63. package/src/tui/app.ts +29 -0
  64. package/src/tui/components/ActionButton.tsx +36 -0
  65. package/src/tui/components/CliModal.tsx +81 -0
  66. package/src/tui/components/CommandSelector.tsx +159 -0
  67. package/src/tui/components/ConfigForm.tsx +148 -0
  68. package/src/tui/components/EditorModal.tsx +177 -0
  69. package/src/tui/components/FieldRow.tsx +30 -0
  70. package/src/tui/components/Header.tsx +31 -0
  71. package/src/tui/components/JsonHighlight.tsx +128 -0
  72. package/src/tui/components/LogsPanel.tsx +86 -0
  73. package/src/tui/components/ResultsPanel.tsx +93 -0
  74. package/src/tui/components/StatusBar.tsx +59 -0
  75. package/src/tui/components/index.ts +13 -0
  76. package/src/tui/components/types.ts +30 -0
  77. package/src/tui/context/KeyboardContext.tsx +118 -0
  78. package/src/tui/context/index.ts +7 -0
  79. package/src/tui/hooks/index.ts +35 -0
  80. package/src/tui/hooks/useClipboard.ts +66 -0
  81. package/src/tui/hooks/useCommandExecutor.ts +131 -0
  82. package/src/tui/hooks/useConfigState.ts +171 -0
  83. package/src/tui/hooks/useKeyboardHandler.ts +91 -0
  84. package/src/tui/hooks/useLogStream.ts +96 -0
  85. package/src/tui/hooks/useSpinner.ts +46 -0
  86. package/src/tui/index.ts +65 -0
  87. package/src/tui/theme.ts +21 -0
  88. package/src/tui/utils/buildCliCommand.ts +90 -0
  89. package/src/tui/utils/index.ts +13 -0
  90. package/src/tui/utils/parameterPersistence.ts +96 -0
  91. package/src/tui/utils/schemaToFields.ts +144 -0
  92. package/src/types/command.ts +103 -0
  93. package/src/types/execution.ts +11 -0
  94. package/src/types/index.ts +1 -0
  95. package/tsconfig.json +25 -0
@@ -0,0 +1,412 @@
1
+ import { test, expect, describe } from "bun:test";
2
+ import {
3
+ generateHelp,
4
+ formatUsage,
5
+ formatCommands,
6
+ formatOptions,
7
+ formatExamples,
8
+ getCommandSummary,
9
+ formatGlobalOptions,
10
+ generateCommandHelp,
11
+ } from "../cli/help.ts";
12
+ import { defineCommand } from "../types/command.ts";
13
+
14
+ describe("Help Generation", () => {
15
+ describe("formatUsage", () => {
16
+ test("formats basic usage", () => {
17
+ const cmd = defineCommand({
18
+ name: "test",
19
+ description: "Test command",
20
+ execute: () => {},
21
+ });
22
+
23
+ const usage = formatUsage(cmd, "myapp");
24
+ expect(usage).toContain("myapp");
25
+ expect(usage).toContain("test");
26
+ });
27
+
28
+ test("includes [command] for commands with subcommands", () => {
29
+ const cmd = defineCommand({
30
+ name: "parent",
31
+ description: "Parent command",
32
+ subcommands: {
33
+ child: defineCommand({
34
+ name: "child",
35
+ description: "Child",
36
+ execute: () => {},
37
+ }),
38
+ },
39
+ execute: () => {},
40
+ });
41
+
42
+ const usage = formatUsage(cmd);
43
+ expect(usage).toContain("[command]");
44
+ });
45
+
46
+ test("includes [options] for commands with options", () => {
47
+ const cmd = defineCommand({
48
+ name: "test",
49
+ description: "Test command",
50
+ options: {
51
+ verbose: { type: "boolean", description: "Verbose" },
52
+ },
53
+ execute: () => {},
54
+ });
55
+
56
+ const usage = formatUsage(cmd);
57
+ expect(usage).toContain("[options]");
58
+ });
59
+ });
60
+
61
+ describe("formatCommands", () => {
62
+ test("formats subcommands list", () => {
63
+ const cmd = defineCommand({
64
+ name: "parent",
65
+ description: "Parent",
66
+ subcommands: {
67
+ child: defineCommand({
68
+ name: "child",
69
+ description: "Child command",
70
+ execute: () => {},
71
+ }),
72
+ },
73
+ execute: () => {},
74
+ });
75
+
76
+ const commands = formatCommands(cmd);
77
+ expect(commands).toContain("child");
78
+ expect(commands).toContain("Child command");
79
+ });
80
+
81
+ test("excludes hidden commands", () => {
82
+ const cmd = defineCommand({
83
+ name: "parent",
84
+ description: "Parent",
85
+ subcommands: {
86
+ visible: defineCommand({
87
+ name: "visible",
88
+ description: "Visible",
89
+ execute: () => {},
90
+ }),
91
+ hidden: defineCommand({
92
+ name: "hidden",
93
+ description: "Hidden",
94
+ hidden: true,
95
+ execute: () => {},
96
+ }),
97
+ },
98
+ execute: () => {},
99
+ });
100
+
101
+ const commands = formatCommands(cmd);
102
+ expect(commands).toContain("visible");
103
+ expect(commands).not.toMatch(/\bhidden\b/);
104
+ });
105
+
106
+ test("shows command aliases", () => {
107
+ const cmd = defineCommand({
108
+ name: "parent",
109
+ description: "Parent",
110
+ subcommands: {
111
+ list: defineCommand({
112
+ name: "list",
113
+ description: "List items",
114
+ aliases: ["ls", "l"],
115
+ execute: () => {},
116
+ }),
117
+ },
118
+ execute: () => {},
119
+ });
120
+
121
+ const commands = formatCommands(cmd);
122
+ expect(commands).toContain("ls");
123
+ expect(commands).toContain("l");
124
+ });
125
+
126
+ test("returns empty for no subcommands", () => {
127
+ const cmd = defineCommand({
128
+ name: "test",
129
+ description: "Test",
130
+ execute: () => {},
131
+ });
132
+
133
+ const commands = formatCommands(cmd);
134
+ expect(commands).toBe("");
135
+ });
136
+ });
137
+
138
+ describe("formatOptions", () => {
139
+ test("formats options with descriptions", () => {
140
+ const cmd = defineCommand({
141
+ name: "test",
142
+ description: "Test",
143
+ options: {
144
+ verbose: { type: "boolean", description: "Enable verbose" },
145
+ },
146
+ execute: () => {},
147
+ });
148
+
149
+ const options = formatOptions(cmd);
150
+ expect(options).toContain("--verbose");
151
+ expect(options).toContain("Enable verbose");
152
+ });
153
+
154
+ test("shows option aliases", () => {
155
+ const cmd = defineCommand({
156
+ name: "test",
157
+ description: "Test",
158
+ options: {
159
+ verbose: { type: "boolean", alias: "v", description: "Verbose" },
160
+ },
161
+ execute: () => {},
162
+ });
163
+
164
+ const options = formatOptions(cmd);
165
+ expect(options).toContain("-v");
166
+ });
167
+
168
+ test("shows default values", () => {
169
+ const cmd = defineCommand({
170
+ name: "test",
171
+ description: "Test",
172
+ options: {
173
+ count: { type: "number", default: 10, description: "Count" },
174
+ },
175
+ execute: () => {},
176
+ });
177
+
178
+ const options = formatOptions(cmd);
179
+ expect(options).toContain("10");
180
+ });
181
+
182
+ test("shows required marker", () => {
183
+ const cmd = defineCommand({
184
+ name: "test",
185
+ description: "Test",
186
+ options: {
187
+ name: { type: "string", required: true, description: "Name" },
188
+ },
189
+ execute: () => {},
190
+ });
191
+
192
+ const options = formatOptions(cmd);
193
+ expect(options).toContain("*");
194
+ });
195
+
196
+ test("shows enum values", () => {
197
+ const cmd = defineCommand({
198
+ name: "test",
199
+ description: "Test",
200
+ options: {
201
+ level: {
202
+ type: "string",
203
+ enum: ["low", "high"],
204
+ description: "Level",
205
+ },
206
+ },
207
+ execute: () => {},
208
+ });
209
+
210
+ const options = formatOptions(cmd);
211
+ expect(options).toContain("low");
212
+ expect(options).toContain("high");
213
+ });
214
+
215
+ test("returns empty for no options", () => {
216
+ const cmd = defineCommand({
217
+ name: "test",
218
+ description: "Test",
219
+ execute: () => {},
220
+ });
221
+
222
+ const options = formatOptions(cmd);
223
+ expect(options).toBe("");
224
+ });
225
+ });
226
+
227
+ describe("formatExamples", () => {
228
+ test("formats examples list", () => {
229
+ const cmd = defineCommand({
230
+ name: "test",
231
+ description: "Test",
232
+ examples: [
233
+ { command: "test --verbose", description: "Run with verbose" },
234
+ ],
235
+ execute: () => {},
236
+ });
237
+
238
+ const examples = formatExamples(cmd);
239
+ expect(examples).toContain("test --verbose");
240
+ expect(examples).toContain("Run with verbose");
241
+ });
242
+
243
+ test("returns empty for no examples", () => {
244
+ const cmd = defineCommand({
245
+ name: "test",
246
+ description: "Test",
247
+ execute: () => {},
248
+ });
249
+
250
+ const examples = formatExamples(cmd);
251
+ expect(examples).toBe("");
252
+ });
253
+ });
254
+
255
+ describe("generateHelp", () => {
256
+ test("generates help with app name and version", () => {
257
+ const cmd = defineCommand({
258
+ name: "root",
259
+ description: "Root command",
260
+ execute: () => {},
261
+ });
262
+
263
+ const help = generateHelp(cmd, { appName: "myapp", version: "1.0.0" });
264
+ expect(help).toContain("myapp");
265
+ expect(help).toContain("1.0.0");
266
+ });
267
+
268
+ test("includes usage section", () => {
269
+ const cmd = defineCommand({
270
+ name: "test",
271
+ description: "Test",
272
+ execute: () => {},
273
+ });
274
+
275
+ const help = generateHelp(cmd);
276
+ expect(help).toContain("Usage:");
277
+ });
278
+
279
+ test("includes command description", () => {
280
+ const cmd = defineCommand({
281
+ name: "test",
282
+ description: "A test command for testing",
283
+ execute: () => {},
284
+ });
285
+
286
+ const help = generateHelp(cmd);
287
+ expect(help).toContain("A test command for testing");
288
+ });
289
+
290
+ test("includes options section", () => {
291
+ const cmd = defineCommand({
292
+ name: "test",
293
+ description: "Test",
294
+ options: {
295
+ verbose: { type: "boolean", description: "Verbose mode" },
296
+ },
297
+ execute: () => {},
298
+ });
299
+
300
+ const help = generateHelp(cmd);
301
+ expect(help).toContain("Options:");
302
+ expect(help).toContain("--verbose");
303
+ });
304
+
305
+ test("generates root help with commands", () => {
306
+ const cmd = defineCommand({
307
+ name: "root",
308
+ description: "Root",
309
+ subcommands: {
310
+ run: defineCommand({
311
+ name: "run",
312
+ description: "Run something",
313
+ execute: () => {},
314
+ }),
315
+ },
316
+ execute: () => {},
317
+ });
318
+
319
+ const help = generateHelp(cmd);
320
+ expect(help).toContain("Commands:");
321
+ expect(help).toContain("run");
322
+ });
323
+ });
324
+
325
+ describe("getCommandSummary", () => {
326
+ test("returns command summary", () => {
327
+ const cmd = defineCommand({
328
+ name: "test",
329
+ description: "A test command",
330
+ execute: () => {},
331
+ });
332
+
333
+ const summary = getCommandSummary(cmd);
334
+ expect(summary).toContain("test");
335
+ expect(summary).toContain("A test command");
336
+ });
337
+
338
+ test("includes aliases in summary", () => {
339
+ const cmd = defineCommand({
340
+ name: "list",
341
+ description: "List items",
342
+ aliases: ["ls"],
343
+ execute: () => {},
344
+ });
345
+
346
+ const summary = getCommandSummary(cmd);
347
+ expect(summary).toContain("ls");
348
+ });
349
+ });
350
+
351
+ describe("formatGlobalOptions", () => {
352
+ test("includes --log-level option", () => {
353
+ const result = formatGlobalOptions();
354
+ expect(result).toContain("--log-level");
355
+ expect(result).toContain("Set log level");
356
+ });
357
+
358
+ test("includes log level choices", () => {
359
+ const result = formatGlobalOptions();
360
+ expect(result).toContain("silly");
361
+ expect(result).toContain("trace");
362
+ expect(result).toContain("debug");
363
+ expect(result).toContain("info");
364
+ expect(result).toContain("warn");
365
+ expect(result).toContain("error");
366
+ expect(result).toContain("fatal");
367
+ });
368
+
369
+ test("includes --detailed-logs option", () => {
370
+ const result = formatGlobalOptions();
371
+ expect(result).toContain("--detailed-logs");
372
+ });
373
+
374
+ test("includes --no-detailed-logs option", () => {
375
+ const result = formatGlobalOptions();
376
+ expect(result).toContain("--no-detailed-logs");
377
+ });
378
+ });
379
+
380
+ describe("generateCommandHelp with global options", () => {
381
+ test("includes Global Options section", () => {
382
+ const cmd = defineCommand({
383
+ name: "test",
384
+ description: "Test command",
385
+ execute: () => {},
386
+ });
387
+
388
+ const help = generateCommandHelp(cmd, "myapp");
389
+ expect(help).toContain("Global Options:");
390
+ expect(help).toContain("--log-level");
391
+ expect(help).toContain("--detailed-logs");
392
+ });
393
+
394
+ test("global options appear after command options", () => {
395
+ const cmd = defineCommand({
396
+ name: "test",
397
+ description: "Test command",
398
+ options: {
399
+ verbose: { type: "boolean", description: "Verbose output" },
400
+ },
401
+ execute: () => {},
402
+ });
403
+
404
+ const help = generateCommandHelp(cmd, "myapp");
405
+ const optionsIndex = help.indexOf("Options:");
406
+ const globalOptionsIndex = help.indexOf("Global Options:");
407
+
408
+ expect(optionsIndex).toBeGreaterThan(-1);
409
+ expect(globalOptionsIndex).toBeGreaterThan(optionsIndex);
410
+ });
411
+ });
412
+ });
@@ -0,0 +1,268 @@
1
+ import { test, expect, describe } from "bun:test";
2
+ import {
3
+ extractCommandChain,
4
+ schemaToParseArgsOptions,
5
+ parseOptionValues,
6
+ validateOptions,
7
+ parseCliArgs,
8
+ } from "../cli/parser.ts";
9
+ 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
+
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 },
208
+ });
209
+
210
+ expect(result.command).toBe(cmd);
211
+ expect(result.commandPath).toEqual(["run"]);
212
+ });
213
+
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 },
224
+ });
225
+
226
+ expect(result.showHelp).toBe(true);
227
+ });
228
+
229
+ test("detects -h flag", () => {
230
+ const cmd = defineCommand({
231
+ name: "run",
232
+ description: "Run command",
233
+ execute: () => {},
234
+ });
235
+
236
+ const result = parseCliArgs({
237
+ args: ["run", "-h"],
238
+ commands: { run: cmd },
239
+ });
240
+
241
+ expect(result.showHelp).toBe(true);
242
+ });
243
+
244
+ test("returns error for unknown command", () => {
245
+ const result = parseCliArgs({
246
+ args: ["unknown"],
247
+ commands: {},
248
+ });
249
+
250
+ expect(result.error?.type).toBe("unknown_command");
251
+ });
252
+
253
+ test("uses default command if provided", () => {
254
+ const cmd = defineCommand({
255
+ name: "default",
256
+ description: "Default command",
257
+ execute: () => {},
258
+ });
259
+
260
+ const result = parseCliArgs({
261
+ args: [],
262
+ commands: { default: cmd },
263
+ defaultCommand: "default",
264
+ });
265
+
266
+ expect(result.command).toBe(cmd);
267
+ });
268
+ });