@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,8 +1,10 @@
1
- import { describe, test, expect, afterEach } from "bun:test";
1
+ import { describe, test, expect } from "bun:test";
2
2
  import { Application } from "../core/application.ts";
3
3
  import { Command } from "../core/command.ts";
4
- import { AppContext } from "../core/context.ts";
5
4
  import type { OptionSchema, OptionValues, OptionDef } from "../types/command.ts";
5
+ import { AppContext } from "../core/context.ts";
6
+ import { LogLevel } from "../core/logger.ts";
7
+ import { KNOWN_COMMANDS } from "../core/knownCommands.ts";
6
8
 
7
9
  // Define a proper option schema
8
10
  const testOptions = {
@@ -21,7 +23,6 @@ class TestCommand extends Command<typeof testOptions> {
21
23
  executedWith: Record<string, unknown> | null = null;
22
24
 
23
25
  override async execute(
24
- _ctx: AppContext,
25
26
  opts: OptionValues<typeof testOptions>
26
27
  ): Promise<void> {
27
28
  this.executedWith = opts as Record<string, unknown>;
@@ -35,17 +36,57 @@ class TuiCommand extends Command<OptionSchema> {
35
36
 
36
37
  executed = false;
37
38
 
38
- override async execute(_ctx: AppContext): Promise<void> {
39
+ override async execute(): Promise<void> {
39
40
  this.executed = true;
40
41
  }
41
42
  }
42
43
 
43
44
  describe("Application", () => {
44
- afterEach(() => {
45
- AppContext.clearCurrent();
46
- });
47
-
48
45
  describe("constructor", () => {
46
+ test("rejects reserved help command definitions", () => {
47
+ class ReservedCommand extends Command<OptionSchema> {
48
+ readonly name = KNOWN_COMMANDS.help;
49
+ readonly description = "tries to override built-in";
50
+ readonly options = {};
51
+
52
+ override async execute(): Promise<void> {}
53
+ }
54
+
55
+ expect(() => {
56
+ new Application({
57
+ name: "test-app",
58
+ version: "1.0.0",
59
+ commands: [new ReservedCommand()],
60
+ });
61
+ }).toThrow(/reserved/i);
62
+
63
+ class SubCommand extends Command<OptionSchema> {
64
+ readonly name = KNOWN_COMMANDS.help;
65
+ readonly description = "user help";
66
+ readonly options = {};
67
+
68
+ override async execute(): Promise<void> {}
69
+ }
70
+
71
+ class ParentCommand extends Command<OptionSchema> {
72
+ readonly name = "parent";
73
+ readonly description = "parent";
74
+ readonly options = {};
75
+
76
+ override subCommands = [new SubCommand()];
77
+
78
+ override async execute(): Promise<void> {}
79
+ }
80
+
81
+ expect(() => {
82
+ new Application({
83
+ name: "test-app",
84
+ version: "1.0.0",
85
+ commands: [new ParentCommand()],
86
+ });
87
+ }).toThrow(/automatically injected/i);
88
+ });
89
+
49
90
  test("creates application with name and version", () => {
50
91
  const app = new Application({
51
92
  name: "test-app",
@@ -56,14 +97,16 @@ describe("Application", () => {
56
97
  expect(app.version).toBe("1.0.0");
57
98
  });
58
99
 
59
- test("creates context and sets as current", () => {
60
- const app = new Application({
100
+ test("creates context as side effect of creating application", () => {
101
+ // side effect of creating an application is setting the current context
102
+ new Application({
61
103
  name: "test-app",
62
104
  version: "1.0.0",
63
105
  commands: [],
64
106
  });
65
- expect(AppContext.hasCurrent()).toBe(true);
66
- expect(app.context).toBe(AppContext.current);
107
+
108
+ expect(AppContext.current.config.name).toBe("test-app");
109
+ expect(AppContext.current.config.version).toBe("1.0.0");
67
110
  });
68
111
 
69
112
  test("registers provided commands", () => {
@@ -91,7 +134,7 @@ describe("Application", () => {
91
134
  version: "1.0.0",
92
135
  commands: [],
93
136
  });
94
- expect(app.registry.has("help")).toBe(true);
137
+ expect(app.registry.has(KNOWN_COMMANDS.help)).toBe(true);
95
138
  });
96
139
 
97
140
  test("injects help subcommand into commands", () => {
@@ -103,32 +146,11 @@ describe("Application", () => {
103
146
  commands: [cmd],
104
147
  });
105
148
  expect(cmd.subCommands).toBeDefined();
106
- expect(cmd.subCommands?.some((c) => c.name === "help")).toBe(true);
107
- });
108
- });
109
-
110
- describe("getContext", () => {
111
- test("returns the application context", () => {
112
- const app = new Application({
113
- name: "test-app",
114
- version: "1.0.0",
115
- commands: [],
116
- });
117
- expect(app.getContext()).toBe(app.context);
149
+ expect(cmd.subCommands?.some((c) => c.name === KNOWN_COMMANDS.help)).toBe(true);
118
150
  });
119
151
  });
120
152
 
121
153
  describe("run", () => {
122
- test("shows help when no args and no default command", async () => {
123
- const app = new Application({
124
- name: "test-app",
125
- version: "1.0.0",
126
- commands: [new TestCommand()],
127
- });
128
- // Should not throw
129
- await app.run([]);
130
- });
131
-
132
154
  test("runs default command when no args", async () => {
133
155
  const cmd = new TuiCommand();
134
156
  const app = new Application({
@@ -137,31 +159,30 @@ describe("Application", () => {
137
159
  commands: [cmd],
138
160
  defaultCommand: "tui-cmd",
139
161
  });
140
- await app.run([]);
162
+ await app.runFromArgs([]);
141
163
  expect(cmd.executed).toBe(true);
142
164
  });
143
165
 
144
- test("runs specified command", async () => {
166
+ test("runs specified command and passes options", async () => {
145
167
  const cmd = new TestCommand();
146
168
  const app = new Application({
147
169
  name: "test-app",
148
170
  version: "1.0.0",
149
171
  commands: [cmd],
150
172
  });
151
- await app.run(["test"]);
152
- expect(cmd.executedWith).not.toBeNull();
173
+
174
+ await app.runFromArgs(["test", "--value", "hello"]);
175
+ expect(cmd.executedWith?.["value"]).toBe("hello");
153
176
  });
154
177
 
155
- test("passes options to command", async () => {
156
- const cmd = new TestCommand();
178
+ test("with no args and no default, prints help (no throw)", async () => {
157
179
  const app = new Application({
158
180
  name: "test-app",
159
181
  version: "1.0.0",
160
- commands: [cmd],
182
+ commands: [new TestCommand()],
161
183
  });
162
- await app.run(["test", "--value", "hello"]);
163
- expect(cmd.executedWith).not.toBeNull();
164
- expect(cmd.executedWith?.["value"]).toBe("hello");
184
+
185
+ await app.runFromArgs([]);
165
186
  });
166
187
  });
167
188
 
@@ -179,7 +200,7 @@ describe("Application", () => {
179
200
  called = true;
180
201
  },
181
202
  });
182
- await app.run(["test"]);
203
+ await app.runFromArgs(["test"]);
183
204
  expect(called).toBe(true);
184
205
  });
185
206
 
@@ -196,7 +217,7 @@ describe("Application", () => {
196
217
  called = true;
197
218
  },
198
219
  });
199
- await app.run(["test"]);
220
+ await app.runFromArgs(["test"]);
200
221
  expect(called).toBe(true);
201
222
  });
202
223
 
@@ -219,11 +240,11 @@ describe("Application", () => {
219
240
  commands: [new ErrorCommand()],
220
241
  });
221
242
  app.setHooks({
222
- onError: async (_ctx, error) => {
243
+ onError: async (error) => {
223
244
  errorCaught = error;
224
245
  },
225
246
  });
226
- await app.run(["error-cmd"]);
247
+ await app.runFromArgs(["error-cmd"]);
227
248
  expect(errorCaught?.message).toBe("Test error");
228
249
  });
229
250
  });
@@ -250,7 +271,6 @@ describe("Application", () => {
250
271
  readonly options = configOptions;
251
272
 
252
273
  override buildConfig(
253
- _ctx: AppContext,
254
274
  opts: OptionValues<typeof configOptions>
255
275
  ): ParsedConfig {
256
276
  buildConfigCalled = true;
@@ -260,7 +280,7 @@ describe("Application", () => {
260
280
  };
261
281
  }
262
282
 
263
- override async execute(_ctx: AppContext, config: ParsedConfig): Promise<void> {
283
+ override async execute(config: ParsedConfig): Promise<void> {
264
284
  receivedConfig = config;
265
285
  }
266
286
  }
@@ -271,7 +291,7 @@ describe("Application", () => {
271
291
  commands: [new ConfigCommand()],
272
292
  });
273
293
 
274
- await app.run(["config-cmd", "--value", "test", "--count", "42"]);
294
+ await app.runFromArgs(["config-cmd", "--value", "test", "--count", "42"]);
275
295
 
276
296
  expect(buildConfigCalled).toBe(true);
277
297
  expect(receivedConfig).toEqual({ value: "test", count: 42 });
@@ -286,7 +306,6 @@ describe("Application", () => {
286
306
  readonly options = testOptions;
287
307
 
288
308
  override async execute(
289
- _ctx: AppContext,
290
309
  opts: OptionValues<typeof testOptions>
291
310
  ): Promise<void> {
292
311
  receivedOpts = opts as Record<string, unknown>;
@@ -299,7 +318,7 @@ describe("Application", () => {
299
318
  commands: [new NoConfigCommand()],
300
319
  });
301
320
 
302
- await app.run(["no-config-cmd", "--value", "hello"]);
321
+ await app.runFromArgs(["no-config-cmd", "--value", "hello"]);
303
322
 
304
323
  expect(receivedOpts).toEqual({ value: "hello" });
305
324
  });
@@ -328,12 +347,12 @@ describe("Application", () => {
328
347
  });
329
348
 
330
349
  app.setHooks({
331
- onError: async (_ctx, error) => {
350
+ onError: async (error) => {
332
351
  errorCaught = error;
333
352
  },
334
353
  });
335
354
 
336
- await app.run(["fail-config", "--value", "test"]);
355
+ await app.runFromArgs(["fail-config", "--value", "test"]);
337
356
 
338
357
  expect(errorCaught?.message).toBe("Config validation failed");
339
358
  });
@@ -349,7 +368,7 @@ describe("Application", () => {
349
368
  });
350
369
 
351
370
  // Should not throw - global option should be parsed and removed
352
- await app.run(["--log-level", "debug", "test", "--value", "hello"]);
371
+ await app.runFromArgs(["--log-level", "debug", "test", "--value", "hello"]);
353
372
  expect(cmd.executedWith?.["value"]).toBe("hello");
354
373
  });
355
374
 
@@ -361,7 +380,7 @@ describe("Application", () => {
361
380
  commands: [cmd],
362
381
  });
363
382
 
364
- await app.run(["test", "--log-level", "debug", "--value", "hello"]);
383
+ await app.runFromArgs(["test", "--log-level", "debug", "--value", "hello"]);
365
384
  expect(cmd.executedWith?.["value"]).toBe("hello");
366
385
  });
367
386
 
@@ -374,14 +393,14 @@ describe("Application", () => {
374
393
  });
375
394
 
376
395
  // All of these should work (case-insensitive)
377
- await app.run(["--log-level", "debug", "test"]);
378
- expect(app.context.logger.getMinLevel()).toBe(2); // Debug = 2
396
+ await app.runFromArgs(["--log-level", "debug", "test"]);
397
+ expect(AppContext.current.logger.getMinLevel()).toBe(LogLevel.debug);
379
398
 
380
- await app.run(["--log-level", "Debug", "test"]);
381
- expect(app.context.logger.getMinLevel()).toBe(2);
399
+ await app.runFromArgs(["--log-level", "Debug", "test"]);
400
+ expect(AppContext.current.logger.getMinLevel()).toBe(LogLevel.debug);
382
401
 
383
- await app.run(["--log-level", "DEBUG", "test"]);
384
- expect(app.context.logger.getMinLevel()).toBe(2);
402
+ await app.runFromArgs(["--log-level", "DEBUG", "test"]);
403
+ expect(AppContext.current.logger.getMinLevel()).toBe(LogLevel.debug);
385
404
  });
386
405
 
387
406
  test("parses --detailed-logs flag", async () => {
@@ -392,7 +411,7 @@ describe("Application", () => {
392
411
  commands: [cmd],
393
412
  });
394
413
 
395
- await app.run(["--detailed-logs", "test"]);
414
+ await app.runFromArgs(["--detailed-logs", "test"]);
396
415
  // Should not throw - flag is recognized
397
416
  expect(cmd.executedWith).not.toBeNull();
398
417
  });
@@ -405,7 +424,7 @@ describe("Application", () => {
405
424
  commands: [cmd],
406
425
  });
407
426
 
408
- await app.run(["--no-detailed-logs", "test"]);
427
+ await app.runFromArgs(["--no-detailed-logs", "test"]);
409
428
  // Should not throw - flag is recognized
410
429
  expect(cmd.executedWith).not.toBeNull();
411
430
  });
@@ -418,8 +437,8 @@ describe("Application", () => {
418
437
  commands: [cmd],
419
438
  });
420
439
 
421
- await app.run(["--log-level=warn", "test"]);
422
- expect(app.context.logger.getMinLevel()).toBe(4); // Warn = 4
440
+ await app.runFromArgs(["--log-level=warn", "test"]);
441
+ expect(AppContext.current.logger.getMinLevel()).toBe(LogLevel.warn);
423
442
  });
424
443
  });
425
444
  });
@@ -1,125 +1,105 @@
1
- import { describe, test, expect } from "bun:test";
1
+ import { describe, expect, test } from "bun:test";
2
2
  import { buildCliCommand } from "../tui/utils/buildCliCommand.ts";
3
3
  import type { OptionSchema } from "../types/command.ts";
4
4
 
5
5
  describe("buildCliCommand", () => {
6
- test("builds command with no options", () => {
7
- const result = buildCliCommand("myapp", ["run"], {}, {});
8
- expect(result).toBe("myapp run");
9
- });
6
+ test("builds CLI commands from schema + values", () => {
7
+ const cases: Array<{
8
+ name: string;
9
+ commandPath: string[];
10
+ schema: OptionSchema;
11
+ values: Record<string, unknown>;
12
+ expected: string;
13
+ }> = [
14
+ {
15
+ name: "no options",
16
+ commandPath: ["run"],
17
+ schema: {},
18
+ values: {},
19
+ expected: "myapp run --mode cli",
20
+ },
21
+ {
22
+ name: "string option",
23
+ commandPath: ["greet"],
24
+ schema: { name: { type: "string", description: "Name" } },
25
+ values: { name: "John" },
26
+ expected: "myapp greet --name John --mode cli",
27
+ },
28
+ {
29
+ name: "quotes strings with spaces",
30
+ commandPath: ["greet"],
31
+ schema: { name: { type: "string", description: "Name" } },
32
+ values: { name: "John Doe" },
33
+ expected: "myapp greet --name \"John Doe\" --mode cli",
34
+ },
35
+ {
36
+ name: "skips empty/undefined/null",
37
+ commandPath: ["run"],
38
+ schema: {
39
+ name: { type: "string", description: "Name" },
40
+ count: { type: "number", description: "Count" },
41
+ },
42
+ values: { name: "", count: null },
43
+ expected: "myapp run --mode cli",
44
+ },
45
+ {
46
+ name: "boolean flags only when needed",
47
+ commandPath: ["run"],
48
+ schema: {
49
+ verbose: { type: "boolean", description: "Verbose" },
50
+ quiet: { type: "boolean", description: "Quiet" },
51
+ },
52
+ values: { verbose: true, quiet: false },
53
+ expected: "myapp run --verbose --mode cli",
54
+ },
55
+ {
56
+ name: "number values",
57
+ commandPath: ["process"],
58
+ schema: { count: { type: "number", description: "Count" } },
59
+ values: { count: 42 },
60
+ expected: "myapp process --count 42 --mode cli",
61
+ },
62
+ {
63
+ name: "array values",
64
+ commandPath: ["process"],
65
+ schema: { files: { type: "array", description: "Files" } },
66
+ values: { files: ["a.txt", "b.txt"] },
67
+ expected: "myapp process --files a.txt --files b.txt --mode cli",
68
+ },
69
+ {
70
+ name: "nested command path",
71
+ commandPath: ["db", "migrate"],
72
+ schema: { force: { type: "boolean", description: "Force" } },
73
+ values: { force: true },
74
+ expected: "myapp db migrate --force --mode cli",
75
+ },
76
+ {
77
+ name: "camelCase to kebab-case",
78
+ commandPath: ["build"],
79
+ schema: {
80
+ outputDir: { type: "string", description: "Output directory" },
81
+ maxRetries: { type: "number", description: "Max retries" },
82
+ },
83
+ values: { outputDir: "/tmp", maxRetries: 3 },
84
+ expected: "myapp build --output-dir /tmp --max-retries 3 --mode cli",
85
+ },
86
+ {
87
+ name: "skips defaults and uses --no- for booleans",
88
+ commandPath: ["run"],
89
+ schema: {
90
+ verbose: { type: "boolean", description: "Verbose", default: false },
91
+ count: { type: "number", description: "Count", default: 10 },
92
+ color: { type: "boolean", description: "Color output", default: true },
93
+ },
94
+ values: { verbose: false, count: 10, color: false },
95
+ expected: "myapp run --no-color --mode cli",
96
+ },
97
+ ];
10
98
 
11
- test("includes string options", () => {
12
- const schema: OptionSchema = {
13
- name: { type: "string", description: "Name" },
14
- };
15
- const values = { name: "John" };
16
-
17
- const result = buildCliCommand("myapp", ["greet"], schema, values);
18
- expect(result).toBe("myapp greet --name John");
19
- });
20
-
21
- test("quotes string values with spaces", () => {
22
- const schema: OptionSchema = {
23
- name: { type: "string", description: "Name" },
24
- };
25
- const values = { name: "John Doe" };
26
-
27
- const result = buildCliCommand("myapp", ["greet"], schema, values);
28
- expect(result).toBe("myapp greet --name \"John Doe\"");
29
- });
30
-
31
- test("excludes empty string values", () => {
32
- const schema: OptionSchema = {
33
- name: { type: "string", description: "Name" },
34
- };
35
- const values = { name: "" };
36
-
37
- const result = buildCliCommand("myapp", ["greet"], schema, values);
38
- expect(result).toBe("myapp greet");
39
- });
40
-
41
- test("includes boolean flags only when true", () => {
42
- const schema: OptionSchema = {
43
- verbose: { type: "boolean", description: "Verbose" },
44
- quiet: { type: "boolean", description: "Quiet" },
45
- };
46
- const values = { verbose: true, quiet: false };
47
-
48
- const result = buildCliCommand("myapp", ["run"], schema, values);
49
- expect(result).toBe("myapp run --verbose");
50
- });
51
-
52
- test("includes number values", () => {
53
- const schema: OptionSchema = {
54
- count: { type: "number", description: "Count" },
55
- };
56
- const values = { count: 42 };
57
-
58
- const result = buildCliCommand("myapp", ["process"], schema, values);
59
- expect(result).toBe("myapp process --count 42");
60
- });
61
-
62
- test("excludes undefined and null values", () => {
63
- const schema: OptionSchema = {
64
- name: { type: "string", description: "Name" },
65
- count: { type: "number", description: "Count" },
66
- };
67
- const values = { name: undefined, count: null };
68
-
69
- const result = buildCliCommand("myapp", ["run"], schema, values);
70
- expect(result).toBe("myapp run");
71
- });
72
-
73
- test("handles array values", () => {
74
- const schema: OptionSchema = {
75
- files: { type: "array", description: "Files" },
76
- };
77
- const values = { files: ["a.txt", "b.txt"] };
78
-
79
- const result = buildCliCommand("myapp", ["process"], schema, values);
80
- expect(result).toBe("myapp process --files a.txt --files b.txt");
81
- });
82
-
83
- test("handles nested command path", () => {
84
- const schema: OptionSchema = {
85
- force: { type: "boolean", description: "Force" },
86
- };
87
- const values = { force: true };
88
-
89
- const result = buildCliCommand("myapp", ["db", "migrate"], schema, values);
90
- expect(result).toBe("myapp db migrate --force");
91
- });
92
-
93
- test("converts camelCase to kebab-case", () => {
94
- const schema: OptionSchema = {
95
- outputDir: { type: "string", description: "Output directory" },
96
- maxRetries: { type: "number", description: "Max retries" },
97
- };
98
- const values = { outputDir: "/tmp", maxRetries: 3 };
99
-
100
- const result = buildCliCommand("myapp", ["build"], schema, values);
101
- expect(result).toContain("--output-dir /tmp");
102
- expect(result).toContain("--max-retries 3");
103
- });
104
-
105
- test("skips values that match defaults", () => {
106
- const schema: OptionSchema = {
107
- verbose: { type: "boolean", description: "Verbose", default: false },
108
- count: { type: "number", description: "Count", default: 10 },
109
- };
110
- const values = { verbose: false, count: 10 };
111
-
112
- const result = buildCliCommand("myapp", ["run"], schema, values);
113
- expect(result).toBe("myapp run");
114
- });
115
-
116
- test("uses --no-flag for false when default is true", () => {
117
- const schema: OptionSchema = {
118
- color: { type: "boolean", description: "Color output", default: true },
119
- };
120
- const values = { color: false };
121
-
122
- const result = buildCliCommand("myapp", ["run"], schema, values);
123
- expect(result).toBe("myapp run --no-color");
124
- });
99
+ for (const c of cases) {
100
+ expect(buildCliCommand("myapp", c.commandPath, c.schema, c.values), c.name).toBe(
101
+ c.expected
102
+ );
103
+ }
104
+ });
125
105
  });