@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,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
|
+
|
|
@@ -1,66 +1,45 @@
|
|
|
1
1
|
import { test, expect, describe } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { CommandRegistry } from "../core/registry.ts";
|
|
3
|
+
import { Command } from "../core/command.ts";
|
|
4
|
+
import type { OptionSchema } from "../types/command.ts";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
name: "test",
|
|
10
|
-
description: "Test command",
|
|
11
|
-
execute: () => {},
|
|
12
|
-
});
|
|
6
|
+
class SimpleCommand extends Command<OptionSchema> {
|
|
7
|
+
readonly name: string;
|
|
8
|
+
readonly description: string;
|
|
9
|
+
readonly options = {} as const;
|
|
13
10
|
|
|
14
|
-
|
|
11
|
+
constructor(name: string, description = "") {
|
|
12
|
+
super();
|
|
13
|
+
this.name = name;
|
|
14
|
+
this.description = description;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
override async execute(): Promise<void> {}
|
|
18
|
+
}
|
|
15
19
|
|
|
20
|
+
describe("CommandRegistry", () => {
|
|
21
|
+
test("registers a command", () => {
|
|
22
|
+
const registry = new CommandRegistry();
|
|
23
|
+
registry.register(new SimpleCommand("test", "Test command"));
|
|
16
24
|
expect(registry.has("test")).toBe(true);
|
|
17
25
|
});
|
|
18
26
|
|
|
19
27
|
test("retrieves command by name", () => {
|
|
20
|
-
const registry =
|
|
21
|
-
const cmd =
|
|
22
|
-
name: "greet",
|
|
23
|
-
description: "Greet command",
|
|
24
|
-
execute: () => {},
|
|
25
|
-
});
|
|
26
|
-
|
|
28
|
+
const registry = new CommandRegistry();
|
|
29
|
+
const cmd = new SimpleCommand("greet", "Greet command");
|
|
27
30
|
registry.register(cmd);
|
|
28
|
-
|
|
29
31
|
expect(registry.get("greet")).toBe(cmd);
|
|
30
32
|
});
|
|
31
33
|
|
|
32
|
-
test("resolves command by alias", () => {
|
|
33
|
-
const registry = createCommandRegistry();
|
|
34
|
-
const cmd = defineCommand({
|
|
35
|
-
name: "list",
|
|
36
|
-
description: "List command",
|
|
37
|
-
aliases: ["ls", "l"],
|
|
38
|
-
execute: () => {},
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
registry.register(cmd);
|
|
42
|
-
|
|
43
|
-
expect(registry.resolve("ls")).toBe(cmd);
|
|
44
|
-
expect(registry.resolve("l")).toBe(cmd);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
34
|
test("returns undefined for unknown command", () => {
|
|
48
|
-
const registry =
|
|
35
|
+
const registry = new CommandRegistry();
|
|
49
36
|
expect(registry.get("unknown")).toBeUndefined();
|
|
50
37
|
});
|
|
51
38
|
|
|
52
39
|
test("lists all commands", () => {
|
|
53
|
-
const registry =
|
|
54
|
-
const cmd1 =
|
|
55
|
-
|
|
56
|
-
description: "A",
|
|
57
|
-
execute: () => {},
|
|
58
|
-
});
|
|
59
|
-
const cmd2 = defineCommand({
|
|
60
|
-
name: "b",
|
|
61
|
-
description: "B",
|
|
62
|
-
execute: () => {},
|
|
63
|
-
});
|
|
40
|
+
const registry = new CommandRegistry();
|
|
41
|
+
const cmd1 = new SimpleCommand("a", "A");
|
|
42
|
+
const cmd2 = new SimpleCommand("b", "B");
|
|
64
43
|
|
|
65
44
|
registry.register(cmd1);
|
|
66
45
|
registry.register(cmd2);
|
|
@@ -71,125 +50,19 @@ describe("createCommandRegistry", () => {
|
|
|
71
50
|
});
|
|
72
51
|
|
|
73
52
|
test("throws on duplicate registration", () => {
|
|
74
|
-
const registry =
|
|
75
|
-
const cmd =
|
|
76
|
-
name: "dup",
|
|
77
|
-
description: "Duplicate",
|
|
78
|
-
execute: () => {},
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
registry.register(cmd);
|
|
82
|
-
|
|
83
|
-
expect(() => registry.register(cmd)).toThrow();
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test("throws on alias conflict", () => {
|
|
87
|
-
const registry = createCommandRegistry();
|
|
88
|
-
const cmd1 = defineCommand({
|
|
89
|
-
name: "first",
|
|
90
|
-
description: "First",
|
|
91
|
-
aliases: ["f"],
|
|
92
|
-
execute: () => {},
|
|
93
|
-
});
|
|
94
|
-
const cmd2 = defineCommand({
|
|
95
|
-
name: "second",
|
|
96
|
-
description: "Second",
|
|
97
|
-
aliases: ["f"],
|
|
98
|
-
execute: () => {},
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
registry.register(cmd1);
|
|
102
|
-
|
|
103
|
-
expect(() => registry.register(cmd2)).toThrow();
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
test("has method checks existence by name", () => {
|
|
107
|
-
const registry = createCommandRegistry();
|
|
108
|
-
const cmd = defineCommand({
|
|
109
|
-
name: "exists",
|
|
110
|
-
description: "Exists",
|
|
111
|
-
execute: () => {},
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
registry.register(cmd);
|
|
115
|
-
|
|
116
|
-
expect(registry.has("exists")).toBe(true);
|
|
117
|
-
expect(registry.has("notexists")).toBe(false);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test("has method checks existence by alias", () => {
|
|
121
|
-
const registry = createCommandRegistry();
|
|
122
|
-
const cmd = defineCommand({
|
|
123
|
-
name: "cmd",
|
|
124
|
-
description: "Cmd",
|
|
125
|
-
aliases: ["c"],
|
|
126
|
-
execute: () => {},
|
|
127
|
-
});
|
|
128
|
-
|
|
53
|
+
const registry = new CommandRegistry();
|
|
54
|
+
const cmd = new SimpleCommand("dup", "Duplicate");
|
|
129
55
|
registry.register(cmd);
|
|
130
|
-
|
|
131
|
-
expect(registry.has("c")).toBe(true);
|
|
56
|
+
expect(() => registry.register(cmd)).toThrow(/already registered/i);
|
|
132
57
|
});
|
|
133
58
|
|
|
134
|
-
test("
|
|
135
|
-
const registry =
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
description: "A",
|
|
139
|
-
execute: () => {},
|
|
140
|
-
});
|
|
141
|
-
const cmdB = defineCommand({
|
|
142
|
-
name: "b",
|
|
143
|
-
description: "B",
|
|
144
|
-
execute: () => {},
|
|
145
|
-
});
|
|
59
|
+
test("names returns command names", () => {
|
|
60
|
+
const registry = new CommandRegistry();
|
|
61
|
+
registry.register(new SimpleCommand("a", "A"));
|
|
62
|
+
registry.register(new SimpleCommand("b", "B"));
|
|
146
63
|
|
|
147
|
-
registry.
|
|
148
|
-
registry.register(cmdB);
|
|
149
|
-
|
|
150
|
-
const names = registry.getNames();
|
|
64
|
+
const names = registry.names();
|
|
151
65
|
expect(names).toContain("a");
|
|
152
66
|
expect(names).toContain("b");
|
|
153
67
|
});
|
|
154
|
-
|
|
155
|
-
test("getCommandMap returns command map", () => {
|
|
156
|
-
const registry = createCommandRegistry();
|
|
157
|
-
const cmdA = defineCommand({
|
|
158
|
-
name: "a",
|
|
159
|
-
description: "A",
|
|
160
|
-
execute: () => {},
|
|
161
|
-
});
|
|
162
|
-
const cmdB = defineCommand({
|
|
163
|
-
name: "b",
|
|
164
|
-
description: "B",
|
|
165
|
-
execute: () => {},
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
registry.register(cmdA);
|
|
169
|
-
registry.register(cmdB);
|
|
170
|
-
|
|
171
|
-
const map = registry.getCommandMap();
|
|
172
|
-
expect(map["a"]).toBe(cmdA);
|
|
173
|
-
expect(map["b"]).toBe(cmdB);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
test("resolve returns undefined for non-existent command", () => {
|
|
177
|
-
const registry = createCommandRegistry();
|
|
178
|
-
expect(registry.resolve("nonexistent")).toBeUndefined();
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
test("get does not resolve aliases", () => {
|
|
182
|
-
const registry = createCommandRegistry();
|
|
183
|
-
const cmd = defineCommand({
|
|
184
|
-
name: "list",
|
|
185
|
-
description: "List command",
|
|
186
|
-
aliases: ["ls"],
|
|
187
|
-
execute: () => {},
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
registry.register(cmd);
|
|
191
|
-
|
|
192
|
-
expect(registry.get("ls")).toBeUndefined();
|
|
193
|
-
expect(registry.resolve("ls")).toBe(cmd);
|
|
194
|
-
});
|
|
195
68
|
});
|