@pablozaiden/terminatui 0.2.0 → 0.3.0-beta-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +14 -2
- package/CLAUDE.md +1 -0
- package/README.md +64 -43
- package/bun.lock +85 -0
- package/examples/tui-app/commands/config/app/get.ts +6 -10
- package/examples/tui-app/commands/config/app/index.ts +2 -6
- package/examples/tui-app/commands/config/app/set.ts +23 -13
- package/examples/tui-app/commands/config/index.ts +2 -6
- package/examples/tui-app/commands/config/user/get.ts +6 -10
- package/examples/tui-app/commands/config/user/index.ts +2 -6
- package/examples/tui-app/commands/config/user/set.ts +6 -10
- package/examples/tui-app/commands/greet.ts +13 -11
- package/examples/tui-app/commands/math.ts +5 -9
- package/examples/tui-app/commands/status.ts +21 -12
- package/examples/tui-app/index.ts +6 -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 +14 -16
- package/guides/08-complete-application.md +12 -42
- 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 +12 -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 +45 -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 -4
- 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 -619
- 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
|
@@ -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 {
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
101
|
+
expect(() => parseOptionValues(schema, { level: "invalid" })).toThrow();
|
|
102
|
+
});
|
|
242
103
|
});
|
|
243
104
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
251
|
-
});
|
|
112
|
+
expect(validateOptions(schema, { name: "ok", count: 5 })).toEqual([]);
|
|
252
113
|
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
261
|
-
|
|
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
|
+
|