@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.
- package/AGENTS.md +43 -0
- package/CLAUDE.md +1 -0
- package/README.md +64 -43
- package/bun.lock +85 -0
- package/examples/tui-app/commands/config/app/get.ts +62 -0
- package/examples/tui-app/commands/config/app/index.ts +23 -0
- package/examples/tui-app/commands/config/app/set.ts +96 -0
- package/examples/tui-app/commands/config/index.ts +28 -0
- package/examples/tui-app/commands/config/user/get.ts +61 -0
- package/examples/tui-app/commands/config/user/index.ts +23 -0
- package/examples/tui-app/commands/config/user/set.ts +57 -0
- package/examples/tui-app/commands/greet.ts +14 -11
- package/examples/tui-app/commands/math.ts +6 -9
- package/examples/tui-app/commands/status.ts +24 -13
- package/examples/tui-app/index.ts +7 -3
- package/guides/01-hello-world.md +7 -2
- package/guides/02-adding-options.md +2 -2
- package/guides/03-multiple-commands.md +6 -8
- package/guides/04-subcommands.md +8 -8
- package/guides/05-interactive-tui.md +45 -30
- package/guides/06-config-validation.md +4 -12
- package/guides/07-async-cancellation.md +15 -69
- package/guides/08-complete-application.md +13 -179
- package/guides/README.md +7 -3
- package/package.json +4 -8
- package/src/__tests__/application.test.ts +87 -68
- package/src/__tests__/buildCliCommand.test.ts +99 -119
- package/src/__tests__/builtins.test.ts +27 -75
- package/src/__tests__/command.test.ts +100 -131
- package/src/__tests__/context.test.ts +1 -26
- package/src/__tests__/helpCore.test.ts +227 -0
- package/src/__tests__/parser.test.ts +98 -244
- package/src/__tests__/registry.test.ts +33 -160
- package/src/__tests__/schemaToFields.test.ts +75 -158
- package/src/builtins/help.ts +19 -4
- package/src/builtins/settings.ts +18 -32
- package/src/builtins/version.ts +4 -4
- package/src/cli/output/colors.ts +1 -1
- package/src/cli/parser.ts +26 -95
- package/src/core/application.ts +192 -110
- package/src/core/command.ts +26 -9
- package/src/core/context.ts +31 -20
- package/src/core/help.ts +24 -18
- package/src/core/knownCommands.ts +13 -0
- package/src/core/logger.ts +39 -42
- package/src/core/registry.ts +5 -12
- package/src/tui/TuiApplication.tsx +63 -120
- package/src/tui/TuiRoot.tsx +135 -0
- package/src/tui/adapters/factory.ts +19 -0
- package/src/tui/adapters/ink/InkRenderer.tsx +135 -0
- package/src/tui/adapters/ink/components/Button.tsx +12 -0
- package/src/tui/adapters/ink/components/Code.tsx +6 -0
- package/src/tui/adapters/ink/components/CodeHighlight.tsx +6 -0
- package/src/tui/adapters/ink/components/Container.tsx +5 -0
- package/src/tui/adapters/ink/components/Field.tsx +12 -0
- package/src/tui/adapters/ink/components/Label.tsx +24 -0
- package/src/tui/adapters/ink/components/MenuButton.tsx +12 -0
- package/src/tui/adapters/ink/components/MenuItem.tsx +17 -0
- package/src/tui/adapters/ink/components/Overlay.tsx +5 -0
- package/src/tui/adapters/ink/components/Panel.tsx +15 -0
- package/src/tui/adapters/ink/components/ScrollView.tsx +5 -0
- package/src/tui/adapters/ink/components/Select.tsx +44 -0
- package/src/tui/adapters/ink/components/Spacer.tsx +15 -0
- package/src/tui/adapters/ink/components/Spinner.tsx +5 -0
- package/src/tui/adapters/ink/components/TextInput.tsx +22 -0
- package/src/tui/adapters/ink/components/Value.tsx +7 -0
- package/src/tui/adapters/ink/keyboard.ts +97 -0
- package/src/tui/adapters/ink/utils.ts +16 -0
- package/src/tui/adapters/opentui/OpenTuiRenderer.tsx +115 -0
- package/src/tui/adapters/opentui/components/Button.tsx +13 -0
- package/src/tui/adapters/opentui/components/Code.tsx +12 -0
- package/src/tui/adapters/opentui/components/CodeHighlight.tsx +24 -0
- package/src/tui/adapters/opentui/components/Container.tsx +56 -0
- package/src/tui/adapters/opentui/components/Field.tsx +18 -0
- package/src/tui/adapters/opentui/components/Label.tsx +15 -0
- package/src/tui/adapters/opentui/components/MenuButton.tsx +14 -0
- package/src/tui/adapters/opentui/components/MenuItem.tsx +29 -0
- package/src/tui/adapters/opentui/components/Overlay.tsx +21 -0
- package/src/tui/adapters/opentui/components/Panel.tsx +78 -0
- package/src/tui/adapters/opentui/components/ScrollView.tsx +85 -0
- package/src/tui/adapters/opentui/components/Select.tsx +59 -0
- package/src/tui/adapters/opentui/components/Spacer.tsx +5 -0
- package/src/tui/adapters/opentui/components/Spinner.tsx +12 -0
- package/src/tui/adapters/opentui/components/TextInput.tsx +13 -0
- package/src/tui/adapters/opentui/components/Value.tsx +13 -0
- package/src/tui/{hooks → adapters/opentui/hooks}/useSpinner.ts +2 -11
- package/src/tui/adapters/opentui/keyboard.ts +61 -0
- package/src/tui/adapters/types.ts +70 -0
- package/src/tui/components/ActionButton.tsx +0 -36
- package/src/tui/components/CommandSelector.tsx +52 -92
- package/src/tui/components/ConfigForm.tsx +68 -42
- package/src/tui/components/FieldRow.tsx +0 -30
- package/src/tui/components/Header.tsx +14 -13
- package/src/tui/components/JsonHighlight.tsx +10 -17
- package/src/tui/components/ModalBase.tsx +38 -0
- package/src/tui/components/ResultsPanel.tsx +27 -36
- package/src/tui/components/StatusBar.tsx +24 -39
- package/src/tui/components/logColors.ts +12 -0
- package/src/tui/context/ClipboardContext.tsx +87 -0
- package/src/tui/context/ExecutorContext.tsx +139 -0
- package/src/tui/context/KeyboardContext.tsx +85 -71
- package/src/tui/context/LogsContext.tsx +35 -0
- package/src/tui/context/NavigationContext.tsx +194 -0
- package/src/tui/context/RendererContext.tsx +20 -0
- package/src/tui/context/TuiAppContext.tsx +58 -0
- package/src/tui/hooks/useActiveKeyHandler.ts +75 -0
- package/src/tui/hooks/useBackHandler.ts +34 -0
- package/src/tui/hooks/useClipboard.ts +40 -25
- package/src/tui/hooks/useClipboardProvider.ts +42 -0
- package/src/tui/hooks/useGlobalKeyHandler.ts +54 -0
- package/src/tui/modals/CliModal.tsx +82 -0
- package/src/tui/modals/EditorModal.tsx +207 -0
- package/src/tui/modals/LogsModal.tsx +98 -0
- package/src/tui/registry.ts +102 -0
- package/src/tui/screens/CommandSelectScreen.tsx +162 -0
- package/src/tui/screens/ConfigScreen.tsx +160 -0
- package/src/tui/screens/ErrorScreen.tsx +58 -0
- package/src/tui/screens/ResultsScreen.tsx +60 -0
- package/src/tui/screens/RunningScreen.tsx +72 -0
- package/src/tui/screens/ScreenBase.ts +6 -0
- package/src/tui/semantic/Button.tsx +7 -0
- package/src/tui/semantic/Code.tsx +7 -0
- package/src/tui/semantic/CodeHighlight.tsx +7 -0
- package/src/tui/semantic/Container.tsx +7 -0
- package/src/tui/semantic/Field.tsx +7 -0
- package/src/tui/semantic/Label.tsx +7 -0
- package/src/tui/semantic/MenuButton.tsx +7 -0
- package/src/tui/semantic/MenuItem.tsx +7 -0
- package/src/tui/semantic/Overlay.tsx +7 -0
- package/src/tui/semantic/Panel.tsx +7 -0
- package/src/tui/semantic/ScrollView.tsx +9 -0
- package/src/tui/semantic/Select.tsx +7 -0
- package/src/tui/semantic/Spacer.tsx +7 -0
- package/src/tui/semantic/Spinner.tsx +7 -0
- package/src/tui/semantic/TextInput.tsx +7 -0
- package/src/tui/semantic/Value.tsx +7 -0
- package/src/tui/semantic/types.ts +195 -0
- package/src/tui/theme.ts +25 -14
- package/src/tui/utils/buildCliCommand.ts +1 -0
- package/src/tui/utils/getEnumKeys.ts +3 -0
- package/src/tui/utils/parameterPersistence.ts +1 -0
- package/src/types/command.ts +0 -60
- package/examples/tui-app/commands/index.ts +0 -3
- package/src/__tests__/colors.test.ts +0 -127
- package/src/__tests__/commandClass.test.ts +0 -130
- package/src/__tests__/help.test.ts +0 -412
- package/src/__tests__/registryNew.test.ts +0 -160
- package/src/__tests__/table.test.ts +0 -146
- package/src/__tests__/tui.test.ts +0 -26
- package/src/builtins/index.ts +0 -4
- package/src/cli/help.ts +0 -174
- package/src/cli/index.ts +0 -3
- package/src/cli/output/index.ts +0 -2
- package/src/cli/output/table.ts +0 -141
- package/src/commands/help.ts +0 -50
- package/src/commands/index.ts +0 -1
- package/src/components/index.ts +0 -147
- package/src/core/index.ts +0 -15
- package/src/hooks/index.ts +0 -131
- package/src/index.ts +0 -137
- package/src/registry/commandRegistry.ts +0 -77
- package/src/registry/index.ts +0 -1
- package/src/tui/TuiApp.tsx +0 -582
- package/src/tui/app.ts +0 -29
- package/src/tui/components/CliModal.tsx +0 -81
- package/src/tui/components/EditorModal.tsx +0 -177
- package/src/tui/components/LogsPanel.tsx +0 -86
- package/src/tui/components/index.ts +0 -13
- package/src/tui/context/index.ts +0 -7
- package/src/tui/hooks/index.ts +0 -35
- package/src/tui/hooks/useKeyboardHandler.ts +0 -91
- package/src/tui/hooks/useLogStream.ts +0 -96
- package/src/tui/index.ts +0 -65
- package/src/tui/utils/index.ts +0 -13
- package/src/types/index.ts +0 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { describe, test, expect
|
|
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(
|
|
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
|
|
60
|
-
|
|
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
|
-
|
|
66
|
-
expect(
|
|
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(
|
|
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 ===
|
|
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.
|
|
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
|
-
|
|
152
|
-
|
|
173
|
+
|
|
174
|
+
await app.runFromArgs(["test", "--value", "hello"]);
|
|
175
|
+
expect(cmd.executedWith?.["value"]).toBe("hello");
|
|
153
176
|
});
|
|
154
177
|
|
|
155
|
-
test("
|
|
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: [
|
|
182
|
+
commands: [new TestCommand()],
|
|
161
183
|
});
|
|
162
|
-
|
|
163
|
-
|
|
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.
|
|
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.
|
|
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 (
|
|
243
|
+
onError: async (error) => {
|
|
223
244
|
errorCaught = error;
|
|
224
245
|
},
|
|
225
246
|
});
|
|
226
|
-
await app.
|
|
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(
|
|
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.
|
|
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.
|
|
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 (
|
|
350
|
+
onError: async (error) => {
|
|
332
351
|
errorCaught = error;
|
|
333
352
|
},
|
|
334
353
|
});
|
|
335
354
|
|
|
336
|
-
await app.
|
|
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.
|
|
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.
|
|
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.
|
|
378
|
-
expect(
|
|
396
|
+
await app.runFromArgs(["--log-level", "debug", "test"]);
|
|
397
|
+
expect(AppContext.current.logger.getMinLevel()).toBe(LogLevel.debug);
|
|
379
398
|
|
|
380
|
-
await app.
|
|
381
|
-
expect(
|
|
399
|
+
await app.runFromArgs(["--log-level", "Debug", "test"]);
|
|
400
|
+
expect(AppContext.current.logger.getMinLevel()).toBe(LogLevel.debug);
|
|
382
401
|
|
|
383
|
-
await app.
|
|
384
|
-
expect(
|
|
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.
|
|
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.
|
|
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.
|
|
422
|
-
expect(
|
|
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,
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
});
|