@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,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
|
});
|
|
@@ -3,7 +3,7 @@ import { AppContext, type AppConfig } from "../core/context.ts";
|
|
|
3
3
|
|
|
4
4
|
describe("AppContext", () => {
|
|
5
5
|
afterEach(() => {
|
|
6
|
-
AppContext.
|
|
6
|
+
AppContext.setCurrent(new AppContext({ name: "empty", version: "0.0.0" }));
|
|
7
7
|
});
|
|
8
8
|
|
|
9
9
|
describe("constructor", () => {
|
|
@@ -22,37 +22,12 @@ describe("AppContext", () => {
|
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
describe("static current", () => {
|
|
25
|
-
test("throws when no current context", () => {
|
|
26
|
-
expect(() => AppContext.current).toThrow(
|
|
27
|
-
"AppContext.current accessed before initialization"
|
|
28
|
-
);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
25
|
test("returns current context after setCurrent", () => {
|
|
32
26
|
const config: AppConfig = { name: "test", version: "1.0.0" };
|
|
33
27
|
const ctx = new AppContext(config);
|
|
34
28
|
AppContext.setCurrent(ctx);
|
|
35
29
|
expect(AppContext.current).toBe(ctx);
|
|
36
30
|
});
|
|
37
|
-
|
|
38
|
-
test("hasCurrent returns false when no context", () => {
|
|
39
|
-
expect(AppContext.hasCurrent()).toBe(false);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test("hasCurrent returns true after setCurrent", () => {
|
|
43
|
-
const config: AppConfig = { name: "test", version: "1.0.0" };
|
|
44
|
-
const ctx = new AppContext(config);
|
|
45
|
-
AppContext.setCurrent(ctx);
|
|
46
|
-
expect(AppContext.hasCurrent()).toBe(true);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("clearCurrent clears the context", () => {
|
|
50
|
-
const config: AppConfig = { name: "test", version: "1.0.0" };
|
|
51
|
-
const ctx = new AppContext(config);
|
|
52
|
-
AppContext.setCurrent(ctx);
|
|
53
|
-
AppContext.clearCurrent();
|
|
54
|
-
expect(AppContext.hasCurrent()).toBe(false);
|
|
55
|
-
});
|
|
56
31
|
});
|
|
57
32
|
|
|
58
33
|
describe("services", () => {
|
|
@@ -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
|
+
});
|