@pablozaiden/terminatui 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -43
- package/package.json +11 -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__/configOnChange.test.ts +63 -0
- 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 +3 -3
- 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/index.ts +22 -137
- 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 +139 -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 +119 -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 +71 -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 +165 -0
- package/src/tui/screens/ErrorScreen.tsx +58 -0
- package/src/tui/screens/ResultsScreen.tsx +68 -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/.devcontainer/devcontainer.json +0 -19
- package/.devcontainer/install-prerequisites.sh +0 -49
- package/.github/workflows/copilot-setup-steps.yml +0 -32
- package/.github/workflows/pull-request.yml +0 -27
- package/.github/workflows/release-npm-package.yml +0 -81
- package/AGENTS.md +0 -31
- package/bun.lock +0 -236
- package/examples/tui-app/commands/config/app/get.ts +0 -66
- package/examples/tui-app/commands/config/app/index.ts +0 -27
- package/examples/tui-app/commands/config/app/set.ts +0 -86
- package/examples/tui-app/commands/config/index.ts +0 -32
- package/examples/tui-app/commands/config/user/get.ts +0 -65
- package/examples/tui-app/commands/config/user/index.ts +0 -27
- package/examples/tui-app/commands/config/user/set.ts +0 -61
- package/examples/tui-app/commands/greet.ts +0 -76
- package/examples/tui-app/commands/index.ts +0 -4
- package/examples/tui-app/commands/math.ts +0 -115
- package/examples/tui-app/commands/status.ts +0 -77
- package/examples/tui-app/index.ts +0 -35
- package/guides/01-hello-world.md +0 -96
- package/guides/02-adding-options.md +0 -103
- package/guides/03-multiple-commands.md +0 -163
- package/guides/04-subcommands.md +0 -206
- package/guides/05-interactive-tui.md +0 -194
- package/guides/06-config-validation.md +0 -264
- package/guides/07-async-cancellation.md +0 -336
- package/guides/08-complete-application.md +0 -537
- package/guides/README.md +0 -74
- 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/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
- package/tsconfig.json +0 -25
|
@@ -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
|
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { test, expect, describe,
|
|
2
|
-
import { createVersionCommand
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { test, expect, describe, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { createVersionCommand } from "../builtins/version.ts";
|
|
3
|
+
import { createHelpCommandForParent, createRootHelpCommand } from "../builtins/help.ts";
|
|
4
|
+
import { Command } from "../core/command.ts";
|
|
5
|
+
import { KNOWN_COMMANDS } from "../core/knownCommands.ts";
|
|
5
6
|
|
|
6
7
|
describe("Built-in Commands", () => {
|
|
7
8
|
let originalLog: typeof console.log;
|
|
@@ -19,27 +20,6 @@ describe("Built-in Commands", () => {
|
|
|
19
20
|
console.log = originalLog;
|
|
20
21
|
});
|
|
21
22
|
|
|
22
|
-
describe("formatVersion", () => {
|
|
23
|
-
test("formats version with commit hash", () => {
|
|
24
|
-
const result = formatVersion("1.0.0", "abc1234567890");
|
|
25
|
-
expect(result).toBe("1.0.0 - abc1234");
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test("formats version with short commit hash", () => {
|
|
29
|
-
const result = formatVersion("1.0.0", "abc1234");
|
|
30
|
-
expect(result).toBe("1.0.0 - abc1234");
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
test("shows (dev) when no commit hash", () => {
|
|
34
|
-
const result = formatVersion("1.0.0");
|
|
35
|
-
expect(result).toBe("1.0.0 - (dev)");
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test("shows (dev) when commit hash is empty", () => {
|
|
39
|
-
const result = formatVersion("1.0.0", "");
|
|
40
|
-
expect(result).toBe("1.0.0 - (dev)");
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
23
|
|
|
44
24
|
describe("VersionCommand", () => {
|
|
45
25
|
test("creates command with name 'version'", () => {
|
|
@@ -70,64 +50,36 @@ describe("Built-in Commands", () => {
|
|
|
70
50
|
});
|
|
71
51
|
});
|
|
72
52
|
|
|
73
|
-
describe("
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
description: "Run something",
|
|
78
|
-
execute: () => {},
|
|
79
|
-
}),
|
|
80
|
-
defineCommand({
|
|
81
|
-
name: "build",
|
|
82
|
-
description: "Build something",
|
|
83
|
-
execute: () => {},
|
|
84
|
-
}),
|
|
85
|
-
];
|
|
86
|
-
|
|
87
|
-
test("creates command with name 'help'", () => {
|
|
88
|
-
const cmd = createHelpCommand({ getCommands: () => mockCommands });
|
|
89
|
-
expect(cmd.name).toBe("help");
|
|
53
|
+
describe("HelpCommand", () => {
|
|
54
|
+
test("createRootHelpCommand creates command with name 'help'", () => {
|
|
55
|
+
const cmd = createRootHelpCommand([], "myapp", "1.0.0");
|
|
56
|
+
expect(cmd.name).toBe(KNOWN_COMMANDS.help);
|
|
90
57
|
});
|
|
91
58
|
|
|
92
|
-
test("
|
|
93
|
-
const cmd =
|
|
94
|
-
expect(cmd.
|
|
59
|
+
test("is hidden in TUI", () => {
|
|
60
|
+
const cmd = createRootHelpCommand([], "myapp", "1.0.0");
|
|
61
|
+
expect(cmd.tuiHidden).toBe(true);
|
|
62
|
+
expect(cmd.supportsTui()).toBe(false);
|
|
95
63
|
});
|
|
96
64
|
|
|
97
|
-
test("
|
|
98
|
-
const cmd =
|
|
99
|
-
|
|
65
|
+
test("execute prints app help", async () => {
|
|
66
|
+
const cmd = createRootHelpCommand([], "myapp", "1.0.0");
|
|
67
|
+
await cmd.execute();
|
|
68
|
+
expect(logOutput.join("\n")).toContain("myapp");
|
|
100
69
|
});
|
|
101
70
|
|
|
102
|
-
test("
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
71
|
+
test("createHelpCommandForParent prints parent help", async () => {
|
|
72
|
+
class ParentCommand extends Command {
|
|
73
|
+
readonly name = "run";
|
|
74
|
+
readonly description = "Run something";
|
|
75
|
+
readonly options = {} as const;
|
|
106
76
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
expect(cmd.options?.["command"]).toBeDefined();
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test("execute calls getCommands", () => {
|
|
113
|
-
const getCommands = mock(() => mockCommands);
|
|
114
|
-
const cmd = createHelpCommand({ getCommands });
|
|
115
|
-
// @ts-expect-error - testing with partial options
|
|
116
|
-
cmd.execute({ options: {}, args: [], commandPath: ["help"] });
|
|
117
|
-
expect(getCommands).toHaveBeenCalled();
|
|
118
|
-
});
|
|
77
|
+
override async execute(): Promise<void> {}
|
|
78
|
+
}
|
|
119
79
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
appName: "myapp",
|
|
124
|
-
});
|
|
125
|
-
cmd.execute({
|
|
126
|
-
options: { command: "run" },
|
|
127
|
-
args: [],
|
|
128
|
-
commandPath: ["help"],
|
|
129
|
-
});
|
|
130
|
-
expect(logOutput.join("")).toContain("run");
|
|
80
|
+
const cmd = createHelpCommandForParent(new ParentCommand(), "myapp", "1.0.0");
|
|
81
|
+
await cmd.execute();
|
|
82
|
+
expect(logOutput.join("\n")).toContain("run");
|
|
131
83
|
});
|
|
132
84
|
});
|
|
133
85
|
});
|
|
@@ -1,157 +1,126 @@
|
|
|
1
1
|
import { test, expect, describe } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
import { Command } from "../core/command.ts";
|
|
3
|
+
import type { OptionSchema, OptionValues } from "../types/command.ts";
|
|
4
|
+
|
|
5
|
+
const testOptions = {
|
|
6
|
+
verbose: {
|
|
7
|
+
type: "boolean",
|
|
8
|
+
description: "Enable verbose output",
|
|
9
|
+
alias: "v",
|
|
10
|
+
},
|
|
11
|
+
name: {
|
|
12
|
+
type: "string",
|
|
13
|
+
description: "Name option",
|
|
14
|
+
},
|
|
15
|
+
} as const satisfies OptionSchema;
|
|
16
|
+
|
|
17
|
+
describe("Command (class-based)", () => {
|
|
18
|
+
test("has name and description", () => {
|
|
19
|
+
class TestCommand extends Command<typeof testOptions> {
|
|
20
|
+
readonly name = "test";
|
|
21
|
+
readonly description = "A test command";
|
|
22
|
+
readonly options = testOptions;
|
|
23
|
+
|
|
24
|
+
override async execute(): Promise<void> {}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const cmd = new TestCommand();
|
|
12
28
|
expect(cmd.name).toBe("test");
|
|
13
29
|
expect(cmd.description).toBe("A test command");
|
|
14
30
|
});
|
|
15
31
|
|
|
16
|
-
test("
|
|
17
|
-
|
|
18
|
-
name
|
|
19
|
-
description
|
|
20
|
-
options
|
|
21
|
-
verbose: {
|
|
22
|
-
type: "boolean",
|
|
23
|
-
description: "Enable verbose output",
|
|
24
|
-
alias: "v",
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
execute: () => {},
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
expect(cmd.options?.["verbose"]).toBeDefined();
|
|
31
|
-
expect(cmd.options?.["verbose"]?.type).toBe("boolean");
|
|
32
|
-
});
|
|
32
|
+
test("has options", () => {
|
|
33
|
+
class TestCommand extends Command<typeof testOptions> {
|
|
34
|
+
readonly name = "test";
|
|
35
|
+
readonly description = "A test command";
|
|
36
|
+
readonly options = testOptions;
|
|
33
37
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
name: "test",
|
|
37
|
-
description: "A test command",
|
|
38
|
-
aliases: ["t", "tst"],
|
|
39
|
-
execute: () => {},
|
|
40
|
-
});
|
|
38
|
+
override async execute(): Promise<void> {}
|
|
39
|
+
}
|
|
41
40
|
|
|
42
|
-
|
|
41
|
+
const cmd = new TestCommand();
|
|
42
|
+
expect(cmd.options["verbose"]?.type).toBe("boolean");
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
test("
|
|
46
|
-
|
|
47
|
-
name
|
|
48
|
-
description
|
|
49
|
-
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
const cmd = defineCommand({
|
|
53
|
-
name: "parent",
|
|
54
|
-
description: "A parent command",
|
|
55
|
-
subcommands: { sub },
|
|
56
|
-
execute: () => {},
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
expect(cmd.subcommands?.["sub"]).toBe(sub);
|
|
60
|
-
});
|
|
45
|
+
test("supports subcommands", () => {
|
|
46
|
+
class SubCommand extends Command<OptionSchema> {
|
|
47
|
+
readonly name = "sub";
|
|
48
|
+
readonly description = "A subcommand";
|
|
49
|
+
readonly options = {} as const;
|
|
61
50
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const cmd = defineCommand({
|
|
65
|
-
name: "test",
|
|
66
|
-
description: "A test command",
|
|
67
|
-
execute: () => {
|
|
68
|
-
executed = true;
|
|
69
|
-
},
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
cmd.execute({ options: {}, args: [], commandPath: ["test"] });
|
|
73
|
-
expect(executed).toBe(true);
|
|
74
|
-
});
|
|
51
|
+
override async execute(): Promise<void> {}
|
|
52
|
+
}
|
|
75
53
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
},
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
await cmd.execute({ options: {}, args: [], commandPath: ["test"] });
|
|
88
|
-
expect(executed).toBe(true);
|
|
89
|
-
});
|
|
54
|
+
class ParentCommand extends Command<OptionSchema> {
|
|
55
|
+
readonly name = "parent";
|
|
56
|
+
readonly description = "A parent command";
|
|
57
|
+
readonly options = {} as const;
|
|
58
|
+
override subCommands = [new SubCommand()];
|
|
59
|
+
|
|
60
|
+
override async execute(): Promise<void> {}
|
|
61
|
+
}
|
|
90
62
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const cmd = defineCommand({
|
|
94
|
-
name: "test",
|
|
95
|
-
description: "A test command",
|
|
96
|
-
options: {
|
|
97
|
-
name: {
|
|
98
|
-
type: "string",
|
|
99
|
-
description: "Name option",
|
|
100
|
-
},
|
|
101
|
-
},
|
|
102
|
-
execute: (ctx) => {
|
|
103
|
-
receivedOptions = ctx.options;
|
|
104
|
-
},
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
cmd.execute({ options: { name: "world" }, args: [], commandPath: ["test"] });
|
|
108
|
-
expect(receivedOptions).toEqual({ name: "world" });
|
|
63
|
+
const cmd = new ParentCommand();
|
|
64
|
+
expect(cmd.getSubCommand("sub")?.name).toBe("sub");
|
|
109
65
|
});
|
|
110
66
|
|
|
111
|
-
test("
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
beforeExecute: () => {
|
|
117
|
-
order.push("before");
|
|
118
|
-
},
|
|
119
|
-
execute: () => {
|
|
120
|
-
order.push("execute");
|
|
121
|
-
},
|
|
122
|
-
});
|
|
67
|
+
test("executes command", async () => {
|
|
68
|
+
class ExecCommand extends Command<typeof testOptions> {
|
|
69
|
+
readonly name = "exec";
|
|
70
|
+
readonly description = "Exec command";
|
|
71
|
+
readonly options = testOptions;
|
|
123
72
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
73
|
+
executedWith: OptionValues<typeof testOptions> | null = null;
|
|
74
|
+
|
|
75
|
+
override async execute(opts: OptionValues<typeof testOptions>): Promise<void> {
|
|
76
|
+
this.executedWith = opts;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const cmd = new ExecCommand();
|
|
81
|
+
await cmd.execute({ verbose: true, name: "world" });
|
|
82
|
+
expect(cmd.executedWith).toEqual({ verbose: true, name: "world" });
|
|
127
83
|
});
|
|
128
84
|
|
|
129
|
-
test("
|
|
85
|
+
test("beforeExecute/afterExecute hooks are callable", () => {
|
|
130
86
|
const order: string[] = [];
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
87
|
+
|
|
88
|
+
class HookCommand extends Command<OptionSchema> {
|
|
89
|
+
readonly name = "hook";
|
|
90
|
+
readonly description = "Hook command";
|
|
91
|
+
readonly options = {} as const;
|
|
92
|
+
|
|
93
|
+
override beforeExecute(): void {
|
|
94
|
+
order.push("before");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
override execute(): void {
|
|
135
98
|
order.push("execute");
|
|
136
|
-
}
|
|
137
|
-
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
override afterExecute(): void {
|
|
138
102
|
order.push("after");
|
|
139
|
-
}
|
|
140
|
-
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
141
105
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
106
|
+
const cmd = new HookCommand();
|
|
107
|
+
cmd.beforeExecute();
|
|
108
|
+
cmd.execute();
|
|
109
|
+
cmd.afterExecute();
|
|
110
|
+
|
|
111
|
+
expect(order).toEqual(["before", "execute", "after"]);
|
|
145
112
|
});
|
|
146
113
|
|
|
147
|
-
test("supports
|
|
148
|
-
|
|
149
|
-
name
|
|
150
|
-
description
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
114
|
+
test("supports tuiHidden", () => {
|
|
115
|
+
class HiddenCommand extends Command<OptionSchema> {
|
|
116
|
+
readonly name = "hidden";
|
|
117
|
+
readonly description = "Hidden";
|
|
118
|
+
readonly options = {} as const;
|
|
119
|
+
override readonly tuiHidden = true;
|
|
120
|
+
|
|
121
|
+
override async execute(): Promise<void> {}
|
|
122
|
+
}
|
|
154
123
|
|
|
155
|
-
expect(
|
|
124
|
+
expect(new HiddenCommand().tuiHidden).toBe(true);
|
|
156
125
|
});
|
|
157
126
|
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { test, expect } from "bun:test";
|
|
2
|
+
import { Command } from "../core/command.ts";
|
|
3
|
+
import type { OptionSchema } from "../types/command.ts";
|
|
4
|
+
|
|
5
|
+
class TestCommand extends Command<typeof TestCommand.Options> {
|
|
6
|
+
static readonly Options = {
|
|
7
|
+
a: { type: "string" as const, description: "a" },
|
|
8
|
+
b: { type: "string" as const, description: "b" },
|
|
9
|
+
} as const satisfies OptionSchema;
|
|
10
|
+
|
|
11
|
+
readonly name = "my";
|
|
12
|
+
readonly description = "my";
|
|
13
|
+
readonly options = TestCommand.Options;
|
|
14
|
+
|
|
15
|
+
public readonly onChangeCalls: Array<[
|
|
16
|
+
string,
|
|
17
|
+
unknown,
|
|
18
|
+
Record<string, unknown>
|
|
19
|
+
]> = [];
|
|
20
|
+
|
|
21
|
+
override execute(): void {}
|
|
22
|
+
|
|
23
|
+
override onConfigChange(
|
|
24
|
+
key: string,
|
|
25
|
+
value: unknown,
|
|
26
|
+
allValues: Record<string, unknown>
|
|
27
|
+
) {
|
|
28
|
+
this.onChangeCalls.push([key, value, allValues]);
|
|
29
|
+
if (key === "a") {
|
|
30
|
+
return { b: "derived" };
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
applyTuiConfigChange(
|
|
36
|
+
key: string,
|
|
37
|
+
value: unknown,
|
|
38
|
+
values: Record<string, unknown>
|
|
39
|
+
): Record<string, unknown> {
|
|
40
|
+
let nextValues: Record<string, unknown> = { ...values, [key]: value };
|
|
41
|
+
|
|
42
|
+
const updates = this.onConfigChange?.(key, value, nextValues);
|
|
43
|
+
if (updates && typeof updates === "object") {
|
|
44
|
+
nextValues = { ...nextValues, ...updates };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return nextValues;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
test("onConfigChange merges returned updates", () => {
|
|
52
|
+
const command = new TestCommand();
|
|
53
|
+
|
|
54
|
+
const next = command.applyTuiConfigChange("a", "new", {
|
|
55
|
+
a: "old",
|
|
56
|
+
b: "oldb",
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(command.onChangeCalls.length).toBe(1);
|
|
60
|
+
expect(command.onChangeCalls[0]?.[0]).toBe("a");
|
|
61
|
+
expect(command.onChangeCalls[0]?.[1]).toBe("new");
|
|
62
|
+
expect(next).toEqual({ a: "new", b: "derived" });
|
|
63
|
+
});
|